...

суббота, 19 апреля 2014 г.

Кого атакует BillGates?

image

Похоже, ботнет BillGates распространяется все больше и больше — уже 4 знакомых человека обратились ко мне с вопросами, как от него избавиться, и что это такое.

Мне удалось заполучить свежую версию, которая нормально работала на моей системе (получала команды с сервера и DOS-ила), и это весело!


Что изменилось?




Модуль «Gates» теперь состоит из 2 модулей: «Beikong» и «Monitor (moni)». Если он запускается по пути /usr/bin/pojie (в моем случае), то запускается moni, если же по какому-то другому пути, то Beikong. Beikong, по сути, является хренотенью, которая переконфигурирует и обновляет другие модули, а moni отслеживает состояние всех модулей (и перезапускает их в случае необходимости), собирает с них статистику и отправляет ее на сервер через Beikong. Если /usr/bin/pojie не существует, то Beikong скопирует себя туда и запустит.

image

image


image


Beikong пишет путь до себя в /tmp/notify.file, а moni пишет свой PID в /tmp/moni.lock.

Gates все так же дропает простой модуль DDoS, запакованный UPX (в моем случае, он опять назывался cupsddh).

Больше никаких серьезных изменений нет.


Время развлекаться!




Так как ботнет заработал у меня на компьютере и даже кого-то начал атаковать, я решил посмотреть, в каком виде ходит трафик между CnC-серверами и ботами. Как и стоило ожидать, никакого шифрования не было, и все ходило в открытом виде.

Нужно заметить, что Gates использует один тип CnC-серверов (для Bill-модуля и отправки статистики через moni), а «Melinda» (тот модуль, который я обозвал «стучащим» в предыдущей статье. На самом деле, это продвинутый DDoS-модуль, и я ошибся. Название в коде не встречалось, и я решил дать такое) другой, и протокол коммуникации у них разный, но сходства есть.

При запуске, оба модуля подключаются к своим серверам и отправляют HELLO-пакет: у Gates он содержит имя ОС, ядра, имя и версию модуля, а Melinda только имя ОС и ядра.

Данные в пакетах я заменил в соответствии с рекомендациями из знаменитого видео.

image


image


Затем, они перекидываются друг с другом PING-пакетами.

Gates CnC может отправить сразу несколько серверов для атаки через cupsddh. Модуль не особо умный, умеет атаковать только по TCP и не умеет подделывать пакеты, чего не скажешь про Melinda, которая умеет атаковать по TCP, UDP, ICMP и 2 типам DNS.


Трекинг




В общем, решил я написать трекер этого ботнета: клиента, который бы подключался к CnC-серверам и получал команды на DDoS. Трекер работает как с серверами Gates, так и с Melinda. И написал.

http://ift.tt/1jl7DsI


Примерно неделю я отслеживал действия ботнета и записывал результаты в базу.


image


image


image


image


Да, я настолько ленивый, что графики рисовал мне phpmyadmin ;)

Отследить действия ботнета в реальном времени вы можете здесь:

billgates.valdikss.org.ru/


Берегите ваши серверы.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


«Универсальная аналитика» от Google вышла из Beta и доступна в Funding.To

image

Учитывая то, что Funding.to, в первую очередь, является инструментом, который рассчитан на проекты, продвигаемые на сторонних ресурсах, эффективность его использования зависит от качества продвижения проекта в сети. Исходя из этого, мы постарались обеспечить наших клиентов всеми необходимыми инструментами для того, чтобы они могли получить информацию, которая поможет разработать тактику и стратегию продвижения проекта в сети. Одним из таких инструментов стала возможность подключения модулей комментирования. Кроме того, мы решили предоставить нашим пользователям возможность подключения статистических систем, таких как Google Analytics и Яндекс Метрика, тем более что второго апреля Google сообщил о том, что их универсальная аналитика выходит из режима бета и становится полноценным инструментом.


Немного предыстории

29 октября 2012 года Google объявили о запуске того, что они назвали «переосмыслением Google Analytics». Задача состояла в том, чтобы создать гибкий и универсальный инструмент, который сможет полноценно использоваться любым сайтом. В том числе были учтены потребности анализа данных о пользователях в зависимости от устройств, которые они используют для взаимодействия с сайтом, а также необходимость простой адаптации статистического инструмента под любой вид деятельности. Результатом стало действительно универсальное решение.


Ориентация на анализ данных о пользователе

imageВыход из беты означает, что в продукте будут доступны все инструменты, в том числе и такие как списки ремаркетинга, демографические отчеты и отчеты по категориям интересов. Также интегрирована система идентификации пользователя, теперь посещение вашего сайта одним и тем же пользователем с различных устройств не будет восприниматься как посещение двумя разными пользователями. Это позволит не только получить более достоверную статистику, но и отслеживать статистику взаимодействий пользователя с вашим сайтом через различные устройства.


Кроме того, в Google утверждают, что новый инструмент поддерживает договор об уровне обслуживания «Премиум», что означает, что все владельцы премиум-аккаунтов, которые перейдут на использование «Универсальной Аналитики», сохранят свои преимущества.


Своевременные данные

Возможность настройки часового пояса позволяет получать данные от Google Analytics более своевременно по сравнению с ситуацией, когда все отчеты формировались по тихоокеанскому времени.


Обновление кода библиотек

Теперь в аккаунт Google Analytics добавлены два поля: IP-адрес и User Agent. Кроме того, Google сообщает о необходимости обновления кода отслеживания через замену библиотеки на протокол передачи статистических данных. Также Google предупреждает, что после интеграции старых отчетов и новых данных возможны некоторые отклонения статистики, которые выровняются с течением времени.


По словам разработчиков, в будущем Universal Analytics станет единственным стандартном аналитики. Переход будет осуществляться в несколько этапов, подробная информация о которых представлена здесь. Кроме того, по ссылке представлена информация о процессе обновления библиотек.


Больше о «Универсальной аналитике» вы можете прочитать здесь.


Funding.To идет в ногу со временем

Наш инструмент позволяет быстро подключить статистические службы от Google и Яндекс к странице вашего проекта для сбора средств. Вам не нужно беспокоиться об обновлении библиотек и других процессах перехода на новые стандарты Google, мы предоставим вам доступ к самым современным инструментам аналитики. В Центре поддержки PAYSTO имеется подробная инструкция, которая поможет провести эту операцию даже тем, кто абсолютно не имеет опыта в этой сфере. Вы получите в свое распоряжение полноценный функционал аналитических инструментов, которые предоставляют крупнейшие поисковые системы.



This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Дайджест интересных материалов из мира веб-разработки и IT за последнюю неделю №105 (13 — 19 апреля 2014)


сегодня в 22:24



Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


[recovery mode] Безопасная платежная карта своими руками

Для оплаты товаров или услуг через интернет платежная карточка не требуется. Достаточно знать всего-навсего три группы цифр (16+4+3):

— номер карты (16 цифр);

— срок действия: месяц и год (4 цифры);

— секретный CVV2 код (3 цифры на обратной стороне).

Если карточка попадает в руки жуликов, переписать указанные 23 цифры можно за считанные секунды. Не говоря уже о ситуации физической утери, от которой не застрахован никто.


Поэтому делюсь своей практикой: от греха подальше CVV2 код с карт Visa и Master стирать. Лучше ножницами. Предварительно, конечно, где-то его продублировав. А для удобства оплаты счетов в интернете написать ключ от кода фломастером (маркером) прямо на карточке.


image

Защищенная карточка (после манипуляций).


Восстановить CVV2 код легко. Составляем нехитрую формулу типа «плюс 4, 0, 4». Забиваем ее себе в память, а когда возникает надобность, быстренько прибавляем цифры формулы к цифрам ключа.

Например:

1) Ключ, написанный фломастером, – 575.

2) Формула восстановления: +4, 0, 4.

3) Вычисляем:

5 + 4 = 9;

7 + 0 = 7;

5 + 4 =9.

Итого: CVV2 код – 979.


Так всего за 5 секунд получаем заветное число, и никакого риска для «кармана».


image

Незащищенная карточка (до манипуляций).


Безопасных вам платежей!


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Организация вебсокетного взаимодействия с приложением Spring

Скажу сразу, что стандартная реализация такого взаимодействия — существует.

Однако, поскольку эта статья — продолжение темы "Простой вызов удалённых сервисных методов в одностраничных приложениях", здесь будет приведена альтернативная схема взаимодействия, необходимая для замены ajax на вебсокеты, в контексте подхода(jrspc), описанного в вышеупомянутой теме.


В первой статье — был описан механизм вызова сервисных методов, с использованием ajax.


В этой статье — описано, как можно реализовать данный механизм, с заменой ajax на вебсокеты,

не меняя код бизнес-логики приложения.


Такая замена даёт более быстрое соединение(тесты в конце), экономию серверной памяти, и добавляет возможность вызывать методы клиента с сервера.


Для демонстрации, написано небольшое чат-приложение, с исходным кодом на гитхабе.

на примере разбора которого, я попытаюсь объяснить, как реализованы клиентская и серверная части такого взаимодействия.

Приложение работает на сервере tomcat 7.042.

Поддерживает https и wss (сертификат неподтверждённый), и не ведёт логов на сервере.



Серверная часть




Основным вопросом, возникшем при организации вебсокетного взаимодействия с приложением Spring, был вопрос — как обеспечить вызов компонентов Spring, из его областей видимостей, которые привязаны к http-сессии, из объекта StreamInbound, возвращаемого методом createWebSocketInbound класса WebSocketServlet, который к сессии не привязан?

Чтобы обеспечить требуемую функциональность по вызову методов серверных компонентов, нам нужно как-то получить доступ к рабочему ApplicationContext из наследника StreamInbound.


Если попытаемся заавтоваерить ApplicationContext, чтобы с его помощью получать нужные нам компоненты, в наследника WebSocketServlet или StreamInbound — нас ждёт разочарование, так как он не проинициализируется, что абсолютно законно.


Для того, чтобы из вебсокетного обработчика получить доступ к компонентам из контекстов Spring, которые связаны с http-сессией, нам нужно создать объект, который бы был сессионным спринговым бином, и который бы хранился в статическом объекте класса-хранилища, доступ к которому, имел бы наследник StreamInbound.


Этот сессионный объект (назовём его ClientManager), создаётся в процессе установки http соединения.


Соответственно, клиент, прежде чем начинать взаимодействовать с сервером через вебсокет, должен сделать один http handshake-запрос, в результате которого, он должен получить ид своего ClientManager.


Результат этого запроса можно передать в код клиента двумя способами — вставить clientManagerId в отдаваемую сгенерированную страницу, или через ajax запрос, со статической страницы (здесь — реализован вариант через ajax).


Обработка этого запроса производится в методе initializeClientManager сессионного контроллера:



@Controller
@Scope("session")
public class ClientManagerController {

@Autowired
private ClientManager clientManager;

@RequestMapping(value = "/init", method = RequestMethod.POST)
@ResponseBody
private String initializeClientManager(HttpSession session) {

JSONObject result = new JSONObject();
try{
boolean loged = ClientManagersStorage.checkClientManager(clientManager, session) ;
result.put("loged", loged);
result.put("clientManagerId", clientManager.getId());
}catch(Throwable th){
result.put("error", th.toString());
}
return result.toString();
}



ClientManagersStorage — это хранилище наших сессионных менеджеров клиентов, имеющее методы по проверке менеджера на null, созданию нового, добавлению в хранилище, поиску и удалению.



public class ClientManagersStorage {

final static private Map<String, ClientManager> clientManagers = new ConcurrentHashMap <String, ClientManager>();

public static boolean checkClientManager(ClientManager clientManager, HttpSession session) {
ClientManager registeredClientManager = clientManagers.get(clientManager.getId());
if (registeredClientManager == null) {
clientManager.setSession(session);
addClientManager(clientManager);
registeredClientManager = clientManager;
}
return registeredClientManager.getUser() != null;
}
...
}


(вопрос управления жизненным циклом сессии будет рассмотрен немного ниже)


Как видно, менеджеры хранятся в статической мапе, по ключу, являющемуся его hashCode, и когда пользователь перезагружает страницу — ему назначается тот же самый менеджер.


Ид этого менеджера — передаётся клиенту в переменной clientManagerId ответа.


После того, как клиент получил ид своего менеджера — он может открывать вебсокетное соединение, передавая свой clientManagerId в единственном параметре запроса на установку соединения.


Запрос на открытие этого соединения обрабатывается в методе createWebSocketInbound класса WebSocketConnectorServlet — имплементации абстрактного WebSocketServlet.



@Override
protected StreamInbound createWebSocketInbound(String paramString, HttpServletRequest request) {
String clientManagerId = request.getParameter("clientManagerId");
ClientManager clientManager = ClientManagersStorage.findClientManager(clientManagerId);
if(clientManager == null){
return new WebSocketConnection(null);
}
log.debug("new connection");
return new WebSocketConnection(clientManager);
}





в нём, из запроса достаётся clientManagerId, по нему находится ClientManager, и создаётся объект WebSocketConnection (являющийся StreamInbound), к которому привязан ClientManager.

Так как ClientManager — сессионный, и был создан в «нормальном» http запросе, то из него будут доступны все спринговые бины, через автоваеренный в него ApllicationContext, который, здесь, будет проинициализирован правильно).


При открытии нового соединения с клиентом, контейнером вызывается метод onOpen класса WebSocketConnection, в котором привязанный к нему ClientManager, добавляет этот WebSocketConnection, в свою мапу соединений, по ид(хэшкоду) объекта.



@Override
protected void onOpen(WsOutbound outbound) {
if(clientManager != null){
clientManager.addConnection(this);
}
}


(Поддержка множества соединений необходима, чтобы пользователь мог открывать приложение в нескольких окнах, каждое из которых будет создавать своё вебсокетное соединение.)


Открыв соединение, клиент может слать запросы на вызовы серверных методов, которые будут обрабатываться в переопределённом методе onTextMessage класса WebSocketConnection.



@Override
protected void onTextMessage(CharBuffer message) throws IOException {
try {
String connectionId = String.valueOf(this.hashCode());
String request = message.toString();
clientManager.handleClientRequest(request, connectionId);
} catch (Throwable th) {
log.error("in onTextMessage: " + th);
}
}


метод handleClientRequest класса ClientManager — обрабатывает запрос, и пишет результат в соединение:



@Autowired
private RequestHandler requestHandler;

public void handleClientRequest(String request, String connectionId) {
log.debug("handleClientRequest request=" + request);
log.debug("handleClientRequest user=" + getUser());
/** handleRequest - never throws exceptions ! */
JSONObject response = requestHandler.handleRequest(request, this);
String responseJson = response.toString();
CharBuffer buffer = CharBuffer.wrap(responseJson);
WebSocketConnection connection = connections.get(connectionId);
try {
connection.getWsOutbound().writeTextMessage(buffer);
} catch (IOException ioe) {
log.error("in handleClientRequest: in writeTextMessage: " + ioe);
}
}


requestHandler — автоваеренный компонент, отвечающий за обработку запросов.

В него вварен ApllicationContext, при помощи которого он находит объекты сервисов.


Его метод handleRequest, ищет компонент сервиса, и вызывает на нём методы нужный клиенту, точно так же, как метод processAjaxRequest из класса CommonServiceController, из предыдущей статьи.


Такова общая схема взаимодействия.


Теперь рассмотрим подробнее момент инициализации ClientManager'а http сессией.


Сессия имеет свойство отваливаться по таймауту, который по умолчанию равен 30 минутам.

Чтобы избежать этого — выставляем его значение на максимум, и инвалидируем сессию когда нам это нужно — а именно, в двух случаях: первый случай — когда кто-то сделал запрос не из приложения, и второй, когда клиент закрыл страницу приложения.


Первый случай обрабатывается прямо в методе инициализации:



public class ClientManager{

public void setSession(HttpSession session) {
/** session will be invalidated at connection removing */
session.setMaxInactiveInterval(Integer.MAX_VALUE);//69.04204112011317 years
this.session = session;
new Thread(new Runnable() {
@Override
public void run() {
/** Giving time to client, for establish websocket connection. */
try {Thread.sleep(60000);} catch (InterruptedException ignored) {}
/** if client not connected via websocket until this time - it is bot */
if (connections.size() == 0) {removeMe();}
}
}).start();
}
private void removeMe() {ClientManagersStorage.removeClientManager(this);}

...
}


а второй — в методе onClose класса WebSocketConnection:



public class WebSocketConnection{
@Override
protected void onClose(int status) {
if(clientManager != null){
clientManager.removeConnection(this);
}
}
...
}


public class ClientManager{

public void removeConnection(WebSocketConnection webSocketConnection) {
String connectionId = getObjectHash(webSocketConnection);
connections.remove(connectionId);
if (connections.size() == 0) {
log.debug("removeConnection before wait: connections.size()=" + connections.size());
/** may be client just reload page? */
try {Thread.sleep(waitForReloadTime);} catch (Throwable ignored) {}
if (connections.size() == 0) {
/** no, client leave us (page closed in browser)*/
ClientManagersStorage.removeClientManager(this);
log.debug("client " + getId() + " disconnected");
}
}
}
...
}


Сессия инвалидируется в методе removeClientManager класса ClientManagersStorage:



public static void removeClientManager(ClientManager clientManager) {
ClientManager removed = clientManagers.remove(clientManager.getId());
if(removed == null){return;}
User user = removed.getUser();
if(user != null){
Broadcaster.broadcastCommand("userPanel.setLogedCount", UserService.logedCount.decrementAndGet());
}
Broadcaster.broadcastCommand("userPanel.setOnlineCount", ClientManagersStorage.getClientManagersCount());
try {
clientManager.getSession().invalidate();
clientManager.setSession(null);
} catch (Throwable th) {
log.error("at removeClientManager: " + th);
}
}




Из этого же метода делается уведомление пользователей о том, что изменилось число посетителей страницы (обработка этих уведомлений на клиенте — описана ниже).

Для уведомления пользователей о событиях на сервере — используется класс Broadcaster, имеющий два метода: broadcastCommand и sendCommandToUser:



public class Broadcaster{

public static void broadcastCommand(String method, Object params) {
for (ClientManager clientManager : ClientManagersStorage.getClientManagers().values()) {
clientManager.sendCommandToClient(method, params);
}
}

public static void sendCommandToUser(Long userId, String method, Object params) {
List<ClientManager> userClientManagers = ClientManagersStorage.findUserClientManagers(userId);
for(ClientManager clientManager: userClientManagers){
clientManager.sendCommandToClient(method, params);
}
}
}


Метод sendCommandToClient класса СlientManager — работает так:



public void sendCommandToClient(String method, Object params) {
for(WebSocketConnection connection: connections.values()){
sendCommandToClientConnection(connection, method, params);
}
}

private void sendCommandToClientConnection(WebSocketConnection connection, String method, Object params) {
JSONObject commandBody = new JSONObject();
if(params == null){params = new JSONObject();}
commandBody.put("method", method);
commandBody.put("params", params);
CharBuffer buffer = CharBuffer.wrap(commandBody.toString());
try {
connection.getWsOutbound().writeTextMessage(buffer);
} catch (IOException ioe) {
log.error("in sendCommandToClient: in writeTextMessage: " + ioe);
}
}


На этом, с серверной частью закончим, и перейдём к клиентской.


Клиентская часть




Клиентская часть должна реализовать три функциональности:

первая — handshake на ajax, для инициализации сессионного СlientManager'а, вторая — вебсокетный транспорт, для отправки запросов jsrpc и получения на них ответов, и третья — вызов функций на клиенте, с сервера.


Первая часть — самая простая:


Поскольку мы используем Ангуляр, для инициализирующего http-сессию запроса ajax, используется $http:



var appName = "jrspc-ws";
var secured = document.location.protocol == "https:" ? "s" : "";
var HttpSessionInitializer = {url: "http"+secured+"://"+ document.location.host +"/"+appName+"/init"};

/** called from root-controller.js after its initialization */
HttpSessionInitializer.init = function($http) {
$http.post(this.url, "").success(function(response){
if (response.error) {
error(response.error);
} else {
loged = response.loged;
Server.initialize("ws"+secured+"://"+ document.location.host +"/"+appName+"/ws?clientManagerId="+response.clientManagerId);
if(loged){Listeners.notify("onLogin");}
}
}).error(function() {error("network error!");});
}


На сервере, этот запрос обрабатывается в методе initializeClientManager класса ClientManagerController, код которого приведён выше, в описании серверной части.


Инициализация сокетного соединения происходит в функции Server.initialize:



connector.initialize = function(url) {
connector.url = url;
try {
connector.connect(url);
return true;
} catch (ex) {
p("in connector.initialize: " + ex);
return false;
}
}


connector — внутренний объект Server, который отвечает за вебсокетное соединение (его полный код находится в файле ws-connector.js)


Код из ws-connector.js, который отвечает за формирование запроса jrspc:



Server.socketRequests = {};

var requestId = 0;

function sendSocket(service, method, params, successCallback, errorCallback, control) {
if (!checkSocket()) {return;}
requestId++;

if(!params){params = [];}
if(!isArray(params)){params = [params];}

var data = {
service : service,
method : method,
params : params,
requestId : requestId
};
Server.socketRequests["request_" + requestId] = {
successCallback : successCallback,
errorCallback : errorCallback,
control : control
};

if (control) {control.disabled = true;}

var message = JSON.stringify(data);
log("sendSocket: "+message);
connector.socket.send(message);
}
...
Server.call = sendSocket;


Код из ws-connector.js, который отвечает за обработку ответов на запросы, и обработку серверных команд:



connector.socket.onmessage = function(message) {
var data = message.data;
var response = JSON.parse(data);
var requestId = response.requestId;
if (requestId) {/** server return response */
var control = Server.socketRequests["request_" + requestId].control;
if (control) {control.disabled = false;}
if (response.error) {
var errorCallback = Server.socketRequests["request_" + requestId].errorCallback;
if (errorCallback) {
try {
errorCallback(response.error);
} catch (ex) {
error("in connector.socket.onmessage errorCallback: " + ex + ", data=" + data);
}
}else{
error(response.error);
}
} else {
var successCallback = Server.socketRequests["request_" + requestId].successCallback;
if (successCallback) {
try {
successCallback(response.result);
} catch (ex) {
error("in connector.socket.onmessage successCallback: " + ex + ", data=" + data);
}
}
}
delete Server.socketRequests["request_" + requestId];
} else {
/** server call client or broadcast */
var method = eval(response.method);
var params = response.params;
try {
method(params);
} catch (ex) {
error("in connector.socket.onmessage call method: " + ex + ", data=" + data);
}
}
};


Применение вышеописанного фреймворка, позволяет реализовать всю бизнес логику, отвечающую за функциональность чата — в двух функциях на клиенте (chat-controller.js):



self.sendMessage = function(command){
var message = {to: (self.sendPrivate ? self.privateTo : "all"), from: userPanel.user.login, text: self.newMessage, clientTime: new Date().getTime()};
Server.call("chatService", "dispatchMessage", message,
function(){ self.newMessage = ""; self.$digest(); }, function(error){self.onError(error);}, command);
}

/** called from server */
self.onChatMessage = function (message){
message.isPrivate = (message.to != "all");
self.messages.push(message);
self.$digest();
chatConsole.scrollTop = chatConsole.clientHeight + chatConsole.scrollHeight;
}


и одном серверном методе:



@Component
public class ChatService extends AbstractService{

@Autowired
private UserManager userManager;

@Secured("User")
@Remote
public void dispatchMessage(ChatMessage message){
message.setServerTime(new Date().getTime());
String to = message.getTo();
if("ALL".equalsIgnoreCase(to)){
Broadcaster.broadcastCommand("chatPanel.onChatMessage", message);
}else{
User fromUser = getUser();
message.setFrom(fromUser.getLogin());
User toUser = userManager.findByLogin(to);
if(toUser == null){throw new RuntimeException("User "+to+" not found!");}
Broadcaster.sendCommandToUser(toUser.getId(), "chatPanel.onChatMessage", message);
Broadcaster.sendCommandToUser(fromUser.getId(), "chatPanel.onChatMessage", message);
}
}
}





Тесты на скорость, при последовательной и параллельной отправке 1000 запросов, для ajax и websockets:

последовательно: ajax (3474, 3380, 3377) ws (1299, 1113, 1054)

параллельно: ajax (1502, 1515, 1469) ws (616, 637, 632)


код тестов


function testController($scope){
var self = $scope;

self.maxIterations = 1000;
self.testIterations = self.maxIterations;
self.testStart = 0;
self.testEnd = 0;

self.testForSpeedSerial = function(command){
if(self.testStart == 0){self.testStart = now();}
if(--self.testIterations <= 0){
var duration = now() - self.testStart;
alert("testForSpeedSerial duration="+duration);
self.testStart = 0;
self.testIterations = self.maxIterations;
return;
}
Server.call("userService", "testForSpeed", "", function(){ self.testForSpeedSerial(command); }, error, command);
}

self.testForSpeedParallelResponses = 0;

self.testForSpeedParallel = function(command){
self.testStart = now();
for(var i = 0; i < self.testIterations; i++){
Server.call("userService", "testForSpeed", "",
function(){
self.testForSpeedParallelResponses++ ;
if(self.testForSpeedParallelResponses >= self.maxIterations){
var duration = now() - self.testStart;
alert("testForSpeedParallel duration="+duration);
self.testForSpeedParallelResponses = 0;
}
}, error, command);
}
}
}





серверный метод testForSpeed:

@Remote public void testForSpeed(){}





Все критические замечания и указания на ошибки буду приняты с благодарностью.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Нейтрализация последствий Heartbleed в Drupal 7

Наверняка все знают, что 8 апреля 2014 Сотрудники The OpenSSL Project выпустили бюллетень безопасности, в котором сообщается о критической уязвимости CVE-2014-0160 в популярной криптографической библиотеке OpenSSL.

Подробнее об этой уязвимости уже писали на хабре, а тут мы рассмотрим как обезопасить свой Drupal сайт.



Проверить подвержен ли ваш сайт уязвимости можно с помощью данного сервиса — http://ift.tt/1kkYl58 (либо по вкусу — http://ift.tt/1eb0FJm или http://ift.tt/1egHnT3).

Т.к. уязвимость касается SSL, то проверять необходимо только HTTPS хосты.

Если вам не повезло и уязвимость есть, то в первую очередь необходимо обновить библиотеку OpenSSL, либо обратиться в поддержку в случае арендуемого хостинга.


Далее рассмотрим пункты, которые я бы рекомендовал выполнить всем, независимо от предыдущего теста, так как к моменту вашей проверки, уязвимость уже могла быть закрыта, а узнать была ли она эксплуатирована нельзя.


Сменить SSL сертификаты




Необходимо сменить SSL сертификаты на основе нового закрытого ключа и отозвать старые сертификаты.

Эти пункты довольно таки специфичные. И зависят от того, где вы покупали сертификаты.

Заменить значения приватных переменных





  • Обновление drupal_private_key

    с помощью hook_update()

    function your_module_update_X() {
    variable_set('drupal_private_key', drupal_random_key());
    }




    либо drush-командой

    drush eval “variable_set('drupal_private_key', drupal_random_key());”




  • Обновление drupal_hash_salt

    с помощью hook_update()

    function your_module_update_X() {
    variable_set('drupal_hash_salt', drupal_hash_base64(drupal_random_bytes(55)));
    }




    либо drush-командой

    drush eval “variable_set('drupal_hash_salt', drupal_hash_base64(drupal_random_bytes(55)));”



Заменить закрытый ключ SSО-аунтентификации




Если вы используете SSO-аунтентификацию, то вам необходимо заменить свой приватный ключ. Например в случаем с модулем Bakery, замену ключа можно произвести на странице настроек модуля.

Удалить активные сессии




Для этого есть три способа:

Учтите, все пользователи сайта окажутся не авторизованными.

Сбросить пароли пользователям с широкими правами доступа (а лучше всем пользователям)




Тут можно посоветовать следующее:


  • уведомить пользователей о необходимости смены пароля, и надеяться на их сознательность

  • насильно сменить пароли пользователям, с соответствующим уведомлением по почте. Для этого может помочь модуль Mass Password Reset

  • если предыдущий вариант по вашему мнению слишком испугает пользователей, можно воспользоваться модулем Password policy. С его помощью можно сделать все пароли “просроченными”, а это значит при следующей авторизации пароль будет необходимо сменить. Вообще этот модуль рекомендую использовать всегда. Он входит в список модулей рекомендованных Аквоей для повышения безопасности сайта.




На этом вроде бы всё.

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Как устроены Яндекс.Карты. Лекция Владимира Зайцева в Яндексе

Яндекс.Карты – это высоконагруженный картографический портал, который работает с огромными объемами данных. В своей лекции Владимир Зайцев рассказывает старшеклассникам – студентам Малого ШАДа – о том, как создавать и поддерживать такие ресурсы, и о технологиях, которые для этого используются. А также на примерах объясняет, какие можно разрабатывать инструменты и проводить исследования на базе полученных данных.

Попробуем разобраться в том, что такое Яндекс.Карты так, как если бы мы ничего о них не знали, и слышим о них впервые. Если смотреть на них в общем, то это большой сайт, на котором собрано несколько сервисов:



  • Спутниковые снимки и карта

  • Народная карта

  • Поиск

  • Пробки

  • Маршрутизатор и навигация

  • Панорамы

  • API




Спутниковые снимки и карта


Конечно же, все начинается с карты. Представим, что мы рассказываем своему приятелю, как добраться до вашей дачи. Ну и нарисовали небольшую карту, по которой он сможет пройти от электрички до вашего дома. Он по этой доге прошел и записал GPS-трек. Если вы потом попробуете наложить этот трек на карту, ничего хорошего из этого не выйдет, они просто не будут совпадать. Как же составить такую карту, которая соответствовала бы реальному GPS-треку? Например, мы при помощи лазерного дальномера с разных точек отмерять расстояния до различных точек на местности, а при помощи компаса вычисляем азимуты. При должной аккуратности измерений, у нас уже есть некоторый шанс, что после наложения у нас получится достаточно точная карта, которая будет совпадать с GPS-треком.

Но если применять этот метод для составления более масштабных карт, процесс окажется слишком длительным и трудоемким. Поэтому сегодня для этих целей применяются более технологичные решения. Например спутниковая фотосъемка. Спутники летают над Землей на высоте 200-500 километров и делают фотоснимки при помощи вот таких объективов:



Спутник вращается вокруг Земли, а наша планета вращается вокруг своей оси, поэтому если спроецировать траекторию спутника на ее поверхность, получится синусоида. При этом траектория спутника при каждом обороте будет немного смещаться, так что через некоторое количество облетов можно будет заснять всю поверхность Земли.


Но есть две проблемы. Во-первых пролеты над какими-то территориями обязательно придутся на ночное время. А ночные спутниковые снимки представляют исключительно эстетическую ценность, для картографии они бессмысленны. Это обязательно нужно учитывать, и снимать только освещенные части, что может потребовать большего количества пролетов. Вторая проблема – это облачность. Если часть города при съемке закрывают облака, то нам потребуется при следующем пролете спутника над ним снимать эту часть заново. Но если в следущий раз спутник полетит над этим городом в другое время, тени будут направлены в совершенно другую сторону. И если мы совместим два снимка, у нас будет ощущение диссонанса. Поэтому такие спутники летают по солнечно-синхронной орбите, рассчитанной таким образом, чтобы каждый пролет спутника над определенной местностью приходился на одно и то же локальное время.


Итак мы произвели фотосъемку, склеили фотографии, получили одно большое полотно в высоком разрешении. Но съемка производилась с какой-то одной точки, так что некоторые углы у нас будут искажаться. Чтобы скомпенсировать искажения нужно всю картинку перепроецировать. Сделать так, будто каждая фотография была сделана именно над этой точкой.


Есть и еще одна проблема спутниковой съемки. Мы ведь хотим получить цветные фотографии, но при проходе через атмосферу световые лучи разного спектра преломляются по-разному. Поэтому цветной снимок высокого разрешения из космоса сделать не получается. Делается по два снимка. Один – черно-белый, высокого разрешения, а второй меньшего разрешения, но цветной. Затем цветной снимок растягивают и накладывают как текстуру на черно-белый. Это становится заметно, когда в кадр попадают быстродвижущиеся объекты, например, самолеты.



Итак, мы произвели съемку, цветокоррекцию и почие необходимые манипуляции, уложили все эти терабайты данных в хранилища, сделали на их основе карты. Но почему бы не отдать пользователям и сами спутниковые снимки. Ведь посмотреть на них тоже полезно и интересно. На Яндекс.Картах эта функция появилась в 2004 году. Интернет тогда был помедленнее, чем сейчас, и передавать большие данных было не так просто. Поэтому большие картинки разбили на тайлы, умещающиеся ровно в один пользовательский экран.


Но вернемся к самим картам. Допустим, мы смогли разметить на снимках дороги, дома, подписать адреса. В итоге у нас получится вот такая картинка:



Теперь нам эту карту нужно раскрасить. Если мы раскрасим кварталы, дома, дороги и водоемы в разные цвета, получится у нас вот что:



Уже не так плохо, но мы-то хотим добиться совсем другого результата:



На самом деле, перед нами стоит сразу несколько задач. Во-первых, выделить из всего многообразия объектов самые значимые, и в зависимости от масштаба добавлять или убирать менее значимые. Кроме того, есть много пользователей с искажениями зрительного восприятия: примерно десять процентов мужчин страдают такими заболеваниями. И это тоже нужно принимать во внимание. На картинке ниже можно посмотреть, как видят карту люди страдающие дальтонизмом:



Как видно, карта остается вполне читаемой, так как цветовая палитра специально подобрана таким образом, чтобы цвета не сливались при наиболее часто встречающихся искажениях зрительного восприятия.


Еще один момент, который нужно учитывать при выборе цветовой палитры заключается в том, что карты часто печатают на черно-белых принтерах, и монохромная карта тоже должна оставаться читаемой:



Народная карта


Россия – огромная страна. Составление подробной карты всей ее территории требует огромных ресурсов. Поэтому, было решено предоставить пользователям возможность самостоятельно размечать те территории, где детализация карт недостаточна. Для этого нужно было реализовать следующие функции:

  • доступ к спутниковым снимкам;

  • инструмент для рисования и подписей;

  • одновременная правка;

  • моментальное отображение;

  • надежность хранилища.




Доступ к спутниковым фотографиям у пользователей на тот момент уже был, все остальные необходимые инструменты также были постепенно созданы. И вот когда была реализована функция онлайн-рендеринга, появилась идея отображения на карте информации о состоянии на дорогах.

Пробки


Подробно о том, как работают Яндекс.Пробки уже рассказывал в своей лекции Леонид Медников, ну а общее представление можно составить из этой картинки:


Досмотрев лекцию до конца, вы узнаете, как устроены не менее интересные компоненты Яндекс.Карт: маршрутизация, Панорамы и API.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Тёплый ламповый свет

Ночь. Компьютер. Интернет.

Три часа, а сна всё нет.


Если вам знакомо состояние, когда засидевшись за компьютером допоздна, вы не ощущаете усталости и не хотите спать, этот пост для вас. В нём попробуем ответить на два классических вопроса: «кто виноват?» и «что делать?».

Мы стали меньше спать




Сравнительно недавно нормальным считался восьмичасовой ночной сон. Современный человек сократил эту норму на четверть и спит в среднем шесть часов в сутки. Немалую роль в сокращении продолжительности сна сыграл компьютер.

Всё больше людей проводит не только вечер, но и часть ночи за компьютером. Кто-то по ночам работает, кто-то не может оторваться от любимой игрушки, кто-то зависает в социальной сети. Но и после того как работа выполнена, уровень в игре пройден, лента новостей прочитана, а время уже далеко за полночь, спать всё равно не хочется. И это совсем нехорошо.


Недосыпание может спровоцировать болезни сердца и нервной системы. Человек, спящий менее 7 часов, теряет до 30% иммунных клеток, защищающих организм. Последствиями недосыпания могут стать проблемы со зрением, раздражительность, депрессия. Увеличивается вероятность возникновения сахарного диабета, онкологических заболеваний.


Кстати, проблемы с весом нередко являются результатом недосыпания. Организм воспринимает отсутствие сна как чрезвычайную ситуацию и начинает запасаться питательными веществами. У спящих менее 6 часов риск ожирения выше в среднем на 23%. У детей недосыпание вызывает задержку роста. Снижается коэффициент интеллекта.


Кто виноват?




Биологические ритмы в нашем организме регулирует мелатонин. Его концентрация повышается в вечернее и ночное время суток, вызывая сонливость. Не являясь снотворным, он облегчает засыпание. Концентрация мелатонина достигает максимума между полуночью и 5 часами утра, в это время синтезируется около 70% его суточного количества.

У мелатонина много полезных свойств. Он усиливает иммунитет, защищает от онкологических заболеваний, снижает тревожность, стабилизирует работу эндокринной системы, участвует в защите организма от неблагоприятных воздействий.

Ещё мелатонин замедляет процессы старения. Одна из теорий старения связывает проблемы, возникающие в пожилом возрасте, со снижением синтеза в организме мелатонина.


Повышение и снижение концентрации мелатонина регулируется количеством света. Когда темнеет, выработка мелатонина увеличивается, и нам хочется спать. Яркое освещение тормозит синтез мелатонина, сон как рукой снимает.


Ученые из университета Томаса Джефферсона провели исследование, в котором приняли участие 72 добровольца (37 женщин, 35 мужчин, средний возраст 24,5 ± 0,3 лет). Исследователи изучали как световые лучи с различными длинами волн – от 440 до 600 нм — влияют на синтез мелатонина.

Добровольцев в течение 90 минут облучали светом с определённой длинной волны и параллельно измеряли изменение уровня содержания мелатонина в их крови.

Выяснилось, что синий свет с длиной волны 446-477 нм подавляет синтез мелатонина больше, чем световые лучи с другими длинами волн.



Почему именно синим лучам принадлежит ведущая роль в подавлении синтеза мелатонина, станет понятно, если вспомнить, что сине-голубые цвета преобладают в дневное время суток, когда человек бодрствует.



Вечереет, на смену холодному синему свету приходит тёплый желто-оранжевый, и синтез мелатонина усиливается, подготавливая организм к ночному сну.


Цветовую температуру света, оказывающего максимальное влияние на подавление синтеза мелатонина, можно определить по формуле:


Цветовая температура = 0,0029/Длина волны


Она составляет около 6500 К. Если вы сейчас посмотрите на цветовую температуру своего монитора (как правило, её можно найти в настройках цвета), скорей всего убедитесь, что она равна 6500 К — именно это значение выставляется на мониторах по умолчанию.

Теперь становится понятным, почему сидя за экраном компьютера или ноутбука не хочется спать: излучаемый им свет разрушает гормон сна мелатонин.


Что делать?




Советовать покупать мелатонин в аптеке не буду, даже несмотря на то, что он считается одним из наиболее безопасных гормональных препаратов. Думаю, нужно дать возможность организму самому справляться с проблемами, а не пичкать его медикаментами.

Наиболее радикальным и, безусловно, действенным решением будет отказаться от использования компьютера, ноутбука и мобильных устройств в вечернее время. Но если этот вариант не подходит, попробуем ограничиться полумерами. Например, можно снизить цветовую температуру экрана монитора до 5000 К. Именно такое значение цветовой температуры имеет полуденное солнце. А вот в вечернее время значение цветовой температуры нашего светила снижается до 4000 К.


Регулировать цветовую температуру экрана монитора в зависимости от времени суток поможет бесплатная мультиплатформенная программа f.lux .

Как выглядит экран вечером и днём можно увидеть здесь. К программе довольно быстро привыкаешь, и через несколько дней не представляешь, как мог без неё обходиться. Особенно разница заметна, когда вечером отключаешь эту программу.

Первая реакция — немедленно включить её обратно — пронзительно белый цвет экрана, просто-таки режет глаза.



О f.lux на хабрахабре уже говорили не раз — в первую очередь как о программе, позволяющей сберечь зрение тем, кто работает с компьютером в вечернее и ночное время. Оказывается, она имеет ещё одно замечательное свойство — уменьшает негативное влияние холодного света монитора на синтез мелатонина, тем самым улучшая наш сон и долголетие.


Программа вполне удобна, хотя сначала может показаться немного непривычной. Попробуйте сделать так. Установите её и пару дней пользуетесь, не меняя никакие настройки. Затем вечером (именно вечером и ночью эффект программы проявляется лучше всего) на время отключаете f.lux. И если у вас не появится немедленного желания включить её обратно — удаляйте. Эта программа вам не подходит.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Обзор Zalman ZM-VE400

ZM-VE400 – это корпус для 2,5” жесткого диска с возможностью шифрования данных (256bit) и эмуляцией CD\DVD\Blu-ray привода.





1. Распаковка




Красивая картонная упаковка с цветной печатью.

4 фото



В коробке:


  • Сам корпус для накопителя

  • Чехол

  • Шнурок USB 3.0

  • 4 винтика и отвертка

  • Руководство пользователя (на английском, французском и испанском языках)




Фото содержимого



В качестве носителя у меня HDD Seagate Momentum 500GB. Вставляется достаточно плотно, не будет болтаться при переносе. Установка настолько тривиальна, что не составит абсолютно никакого труда. Ни каких защелок. 2 болтика надежно крепят крышку корпуса.

Корпус выполнен из алюминия (задняя крышка), акрилового стекла (передняя панель) и пластика (верхняя и нижняя часть)

Размеры: 146х80.1х14 мм.

Вес корпуса без hdd – 91 грамм.

Фото процесса установки



2. Первый запуск




До этого корпуса у меня уже был zm-ve200, который сгорел при попытке перепаять usb-разъем. На жестком диске остались данные и папка _ISO, где хранятся образы дисков.

После включения жесткий диск определился сразу и папка _ISO тоже, на предыдущей версии этого девайса были пляски с прошивкой, т.к. он не видел 500гб диски.

На цифровой клавиатуре нанесены стрелки. Выбрав нужный iso образ и нажав цифру 5 мы получим usb cd-rom с вставленным диском. Можно на лету менять образ. Это очень удобная функция в эпоху ультрабуков и серверов без cd привода.

В самом верху корпуса располагается белый светодиод, который моргает по мере обмена данными с hdd.

Клавиатура Touchscreen. На всей лицевой поверхности нанесена пленка.

На дисплее устройства отображается:


  • Название образа

  • Замочек – можно или нельзя писать на hdd

  • Режим работы устройства: cd диск (O), hdd (H), cd и hdd (D)

  • Индикатор подключения iso

  • Режим подключения: USB2.0 или USB3.0




На клавиатуре:


  • Цифры от 0 до 9.

  • Кнопки Menu и Enter


3. Меню




В меню имеются 6 пунктов:


  • Mode select. В данном пункте можно выбрать Режим работы устройства (Dual, HDD, ODD)

  • Encryprion. Тут задается пароль при подключении устройства к ПК. Будьте осторожны, т.к. все данные при шифровании и его отмене стираются!

  • Settings. Настройки устройства: яркость дисплея в активном режиме, яркость в режиме ожидания, время перехода в режим ожидания.

  • Information. S.M.A.R.T – температура и health (у меня 34 и good), HDD Model, HDD Serial, Firmware Version, USB Speed, USB Input Voltage.

  • USB Connect. В данном разделе есть Refresh – для имитации переподключения провода и Safe Removal – для безопасного отключения устройства.

  • Advanced. Тут можно установить защиту на запись на hdd, а так же «отмантировать» iso.




4. Шифрование




Сначала выбираем в меню устройства Encryprion. Там соглашаемся с потерей данных и устанавливаем пароль. Через некоторое время устройство перезагрузится и предложит ввести пароль. Дается 10 попыток.

Затем на экране появится надпись 1st Partishion: 0. Диск не отформатирован. Необходимо отформатировать диск и можно переносить файлы.

После шифрования скорость передачи данных снизилась с 29 до 25 мб в секунду по usb2.0.

Вместо заключения:




В связи с последними новостями в области законодательства, думаю будет уместно добавить к программным средствам еще и такое устройство.

Подключил к компьютеру, загрузился с livecd, поработал с данными на диске, отключился. И ни каких следов.

Стоимость устройства: 2010 руб.

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Произвольный порядок списка инициализации шаблона

Думаю многие кто работает с шаблонами, знакомы со следующей ситуацией. У нас есть некий шаблонный класс с кучей шаблонных параметров

struct deferred;
struct deadline;
struct disable;

template<class T, class Deferred = disable, class Deadline = disable>
struct some_container




Пускай это будет некий интерфейс отложенного выполнения задач (активная очередь). И мы хотим расширить её функционал добавив отложенное выполнение и дедлайны. Но не хотим что бы все это было сразу включено, а хотим что бы можно было собирать нужную конфигурацию под свои нужды. Проблема таких шаблонов (у которых много параметров и почти все уже имею значение по умолчанию) в том, что для того что бы переопределить, скажем, последний параметр нам нужно указать все перед ним стоящие.

typedef some_container<int, disable, deadline> deadline_container;
<source>
А хотелось бы

<source lang=cpp>
typedef some_container<int, deadline> deadline_container;




А ещё лучше, что бы даже порядок задания не имел значение и следующие два, были бы эквивалентны

typedef some_container<int, deferred, deadline> full_container1;
typedef some_container<int, deadline, deferred> full_container1;




Но мы прекрасно понимаем, что как только мы поменяли два параметр у нас получится совершенно не то чего мы ожидали (это вам не tuple где порядок указание не имеет значении)

Думаю многие уже подумали о том, что всего этого можно добиться добавив прослойку между нашим типом и пользователем, для которой написать все возможные специализации. Если у вас только 2 шаблонных параметр то да, если 3 то уже сложно, а если добавится 4, 5 то пиши пропало. Да и как правило добавление нового параметра приводит к переделыванию всех предыдущих специализаций (так как в специализации мы не можем увеличивать число шаблонных параметров, а можем их только уменьшать).

Если вас заинтересовало, прошу под кат, я покажу вам как добиться этого



Но для начала немного дёгтя. Шаблонные типы хороши тем, что пользователь может параметризовать их разными типами, в том числе и своими. Способ который я хочу показать не позволяет специализировать шаблон произвольным типом. Т.е. например вы определи свой тип deadline_super который сопоставим с типом deadline, тогда вы можете подставлять его в специализацию шаблона

typedef some_container<int, disable, deadline_super>




Но вы не сможете использовать этот тип в том механизме который позволят специализировать шаблон в произвольном виде. Пожалуй это единственное серьёзное ограничение. Учитывая этот аспект становится понятно, что этот механизм удобно использовать при написании модульных компонент, а не расширяемых или полиморфных.

Вся реализация основывается на такой компоненте boost как mpl::set. Я не буду много рассказывать о том, что такое boost::mpl, скажу лишь что mpl::set позволяет создавать аналогичный std::set контейнер но на этапе компиляции и состоящий из типов.

Первое что нам потребуется это способ проверки списка типов на наличие нужного нам типа, и выдачу некоего дефолтного значения (struct disable) в противном случае

template <class Plugin, class IsSet>
struct get_plugin_impl;

template <class Plugin>
struct get_plugin_impl<Plugin, boost::mpl::false_>
{
typedef disable_plugin type;
};

template <class Plugin>
struct get_plugin_impl<Plugin, boost::mpl::true_>
{
typedef Plugin type;
};

template <class List, class Plugin>
struct get_plugin
{
typedef typename get_plugin_impl<Plugin,
typename boost::mpl::has_key<List, Plugin>::type>::type type;
};




Это позволит нам из списка типов узнать был ли задан нужный нам тип и если нет, то использовать дефолтное значение. Как видите данный код зависит только от одного типа который участвует в параметризации конечной структуры (и сообщает что мы не используем это конкретный тип), поэтому при добавлении новых шаблонных типов, этот код не будет меняться.

На следующем шаге, мы определяем все типы которые используются в конечном шаблоне

template <class List>
struct get_plugins
{
typedef typename get_plugin<List, deferred_plugin>::type deferred;
typedef typename get_plugin<List, deadline_plugin>::type deadline;
};




И это то место, которое будет меняться при добавлении новых шаблонных параметров. Но это происходит предельно легко, и никаких 2^n комбинаций перечислять не надо.

Дальше мы вводим прослойку между конечным шаблонным типом и пользователем

template<class T,
class P1 = disable_plugin,
class P2 = disable_plugin>
struct container
{
typedef boost::mpl::set<P1, P2> plugin_list;
typedef get_plugins<plugin_list> plugs;

typedef typename plugs::deferred deferred;
typedef typename plugs::deadline deadline;

typedef some_container<T, deferred, deadline> type;
};




Именно она позволяет нам абстрагироваться от числа и порядка указания шаблонных параметров. По сути она объединяет в себе все те (2^n + K) специализаций который нам пришлось бы писать для различного числа заданных шаблонных параметров и их порядка.

Возвращаясь нашему шаблонному типу, я покажу вам как это работает



#define static_type(name) \
static const std::string& type() \
{ \
static std::string type_(name); \
return type_; \
} \

struct deferred_plugin
{ static_type("deferred"); };

struct deadline_plugin
{ static_type("deadline"); };

struct disable_plugin
{ static_type("disable"); };

template<class T, class Deferred, class Deadline>
struct some_container
{
static const std::string& type()
{
static std::string type_("some_container<" +
Deferred::type() + ", " +
Deadline::type() + ">");
return type_;
}
};




Использование

cout << container<int>::type::type() << std::endl;
cout << container<int, deadline_plugin>::type::type() << std::endl;
cout << container<int, deferred_plugin>::type::type() << std::endl;
cout << container<int, deferred_plugin, deadline_plugin>::type::type() << std::endl;
cout << container<int, deadline_plugin, deferred_plugin>::type::type() << std::endl;




Выхлоп

some_container<disable, disable>

some_container<disable, deadline>

some_container<deferred, disable>

some_container<deferred, deadline>

some_container<deferred, deadline>





Обратите внимание, что порядок специализации сохранен в нужном виде, не зависимо от того в каком порядке были указаны типы при инстанцировании шаблона.

Вот и все, надеюсь кому то это поможет сделать код более красивым и аккуратным.

Исходникик: git


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Kenju форк Kendo UI Web (GPL3)


сегодня в 15:08


Несколько дней назад произошло одно важное событие в развитии JS фреймворка Kendo UI от компании Telerik. Они выпустили OpenSource версию продукта, под лицензией Apache v2 — Kendo UI Core. На деле же всё оказалось не так просто и однозначно.

Ранее вся библиотека Kendo UI выпускалась под лицензией GPL v3, что не разрешало использовать её бесплатно в коммерческих продуктах, для коммерческих приложений на основе этой библиотеки была разработана Kendo UI commercial license. Но для OpenSource же эта библиотека была настоящим подарком. Конечно же есть и другие аналогичные библиотеки и у них огромное количество своих поклонников. Плюсами Kendo можно считать:

1) Полная поддержка JQuery

2) Большое количество виджетов, что позволяет обойтись одной JS библиотекой (не считая jquery) в большинстве случаев

3) Качественные и приятные темы оформления

4) Отличная поддержка Twitter Bootstrap. Для kendo даже есть своя тема оформления bootstrap, для одновременной работы с CSS фреймворком.


Лицензия Apache v2 позволяет использовать библиотеку Kendo UI в коммерческих проектах и на этом все плюсы заканчиваются, так как версия Kendo UI Core содержит не все виджеты из привычной, а именно в неё не входят:

1) Editor

2) Grid

3) Scheduler

4) Treeview

5) Upload


Аналогов, конечно же, много, но не забываем, что аналоги – это дополнительные JS библиотеки и другой стиль оформления (css).


Предыдущие версии фреймворка (GPL v3) очень быстро исчезают из публичного доступа, поэтому версию kendoui.web.2013.3.1119.open-source я выложил на github

Так как название Kendo зарегистрировано Telerik, я переименовал фреймворк в Kenjutsu, а после сокращения получается более звучное Kenju.

Главная задача форка — сохранить то состояние фреймворка под лицензией GPL v3, в котором можно пользоваться недостающими виджетами согласно лицензии GPL v3.




Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Кодирование бинарных данных в строку с алфавитом произвольной длины (BaseN)

Всем хорошо известен алгоритм преобразования массива байт в строку base64. Существует большое количество разновидностей данного алгоритма с различными алфавитами, с хвостовыми символами и без. Есть модификации алгоритма, в котором длина алфавита равна другим степеням двойки, например 32, 16. Однако существуют и более интересные модификации, в которых длина алфавита не кратна степени двойки, такими являются алгоритмы base85, base91. Однако мне не попадался алгоритм, в котором алфавит мог бы быть произвольным, в том числе большей длины, чем 256. Задача показалась мне интересной, и я решил ее реализовать.

Сразу выкладываю ссылку на исходники и демо на js . Хотя и разработанный алгоритм имеет скорее теоретическое значение, я посчитал нужным описать детали его реализации. Практически его можно использовать, например, для случаев, когда меньшая длина строки актуальней, чем ее размер в байтах (например, в квайнах).



Произвольный алфавит



Чтобы понять как можно синтезировать такой алгоритм, я решил разобраться в частном случае, а именно алгоритме base85. Идея у этого алгоритма следующая: входной поток данных разделяется на блоки по 4 байта, затем каждый их них рассматривается как 32-битное число со старшим байтом в начале. Последовательным делением каждого блока на 85 получается 5 цифр 85-ричной системы счисления. Далее каждая цифра кодируется печатным символом из алфавита, размером 85 символов, и выводится в выходной поток, с сохранением порядка, от старшего разряда к младшему.

Но почему был выбран размер 4 байта, т.е. 32 бита? А потому что при этом достигается оптимальное сжатие, т.е. задействовано минимальное количество бит (2^32 = 4294967296) при максимальном количестве символов (85^5 = 4437053125). Однако данную методику можно расширить и для любого другого алфавита. Таким образом была составлена математическая система для поиска количества бит, при котором сжатие будет максимальным:


a — размер алфавита A.

k — количество кодируемых символов.

b — основание системы счисления.

n — количество бит в системе счисления b для представления k символов алфавита A.

r — коэффициент сжатия (чем больше — тем лучше).

mbc — максимальный размер блока в битах.


⌊x⌋ — наибольшее целое, меньшее x (floor).

⌈x⌉ — наименьшее целое, большее x (ceiling).


Далее с помощью данной методики были найдены оптимальные комбинации бит и закодированных в них символов, которые были изображены на рисунке ниже. Максимальный размер блока — 64 бита (такое число было выбрано из-за того, что при большем количестве необходимо использовать большие числа). Как видим, коэффициент сжатия не всегда увеличивается при увеличении количества символов (это видно в области от 60 и от 91). Красными столбиками изображены известные кодировки (85 и 91). Собственно из диаграммы можно сделать вывод, что такое количество символов для этих кодировок было выбрано не зря, поскольку при этом используется минимальное количество бит при хорошем коэффициенте сжатия. Стоит отметить, что при увеличении максимального размера блока, диаграмма может и измениться (например, для кодировки base85 при 64 битах размер блока будет равен 32 битам, а количество символов — 5 при избыточности 1.25. Если же максимальный размер блока увеличить до 256 бит, то размер блока будет равен 141 биту при 22 символах и избыточности 1.2482).



Этапы кодирования



Итак, поэтапно процесс кодирования выглядит следующим образом:


  1. Расчет оптимального размера блока (количество бит n) и количества соответствующих ему символов (k).

  2. Представление исходной строки в виде последовательности байтов (используется UTF8).

  3. Разбиение исходной последовательности байтов на группы по n бит.

  4. Преобразование каждой группы бит в число с системой счисления с основанием a.

  5. Просчет хвостовых бит.




Выполнение первого этапа было рассмотрено выше. Для второго этапа использовался метод для представления строки в виде массива байт (в C# он встроенный Encoding.UTF8.GetBytes(), а для JS он был написан вручную strToUtf8Bytes и bytesToUtf8Str). Далее последующие три этапа будут рассмотрены подробней. Обратное преобразование последовательности символов в массив байт выглядит аналогичным образом.

Кодирование блока бит


private void EncodeBlock(byte[] src, char[] dst, int ind)
{
int charInd = ind * BlockCharsCount;
int bitInd = ind * BlockBitsCount;
BigInteger bits = GetBitsN(src, bitInd, BlockBitsCount);
BitsToChars(dst, charInd, (int)BlockCharsCount, bits);
}

private void DecodeBlock(string src, byte[] dst, int ind)
{
int charInd = ind * BlockCharsCount;
int bitInd = ind * BlockBitsCount;
BigInteger bits = CharsToBits(src, charInd, (int)BlockCharsCount);
AddBitsN(dst, bits, bitInd, BlockBitsCount);
}


GetBitsN возвращает число длины BlockBitsCount бит и начинающееся с бита под номером bitInd.

AddBitsN присоединяет число bits длины BlockBitsCount бит к массиву байт dst на позиции bitInd.


Конвертация блока бит в систему счисления с произвольным основанием и обратно



Alphabet — произвольный алфавит. Для обратного преобразования используется заранее просчитанный обратный алфавит InvAlphabet. Стоит отметить, что, например, для base64 используется прямой порядок бит, а для base85 — обратный (ReverseOrder), _powN — степени длины алфавита.

private void BitsToChars(char[] chars, int ind, int count, BigInteger block)
{
for (int i = 0; i < count; i++)
{
chars[ind + (!ReverseOrder ? i : count - 1 - i)] = Alphabet[(int)(block % CharsCount)];
block /= CharsCount;
}
}

private BigInteger CharsToBits(string data, int ind, int count)
{
BigInteger result = 0;
for (int i = 0; i < count; i++)
result += InvAlphabet[data[ind + (!ReverseOrder ? i : count - 1 - i)]] * _powN[BlockCharsCount - 1 - i];
return result;
}


Обработка хвостовых бит



На решение данной проблемы было потрачено больше всего времени. Однако в итоге получилось создать код для расчета основных и хвостовых бит и символов без использования вещественных чисел, что позволило легко распараллелить алгоритм.

  • mainBitsLength — основное количество бит.

  • tailBitsLength — хвостовое количество бит.

  • mainCharsCount — основное количество символов.

  • tailCharsCount — хвостовое количество символов.

  • globalBitsLength — общее количество бит (mainBitsLength + tailBitsLength).

  • globalCharsCount — общее количество символов (mainCharsCount + tailCharsCount).


Расчет основного и хвостового количества бит и символов при кодировании.


int mainBitsLength = (data.Length * 8 / BlockBitsCount) * BlockBitsCount;
int tailBitsLength = data.Length * 8 - mainBitsLength;
int globalBitsLength = mainBitsLength + tailBitsLength;
int mainCharsCount = mainBitsLength * BlockCharsCount / BlockBitsCount;
int tailCharsCount = (tailBitsLength * BlockCharsCount + BlockBitsCount - 1) / BlockBitsCount;
int globalCharsCount = mainCharsCount + tailCharsCount;
int iterationCount = mainCharsCount / BlockCharsCount;


Расчет основного и хвостового количества бит и символов при декодировании.


int globalBitsLength = ((data.Length - 1) * BlockBitsCount / BlockCharsCount + 8) / 8 * 8;
int mainBitsLength = globalBitsLength / BlockBitsCount * BlockBitsCount;
int tailBitsLength = globalBitsLength - mainBitsLength;
int mainCharsCount = mainBitsLength * BlockCharsCount / BlockBitsCount;
int tailCharsCount = (tailBitsLength * BlockCharsCount + BlockBitsCount - 1) / BlockBitsCount;
BigInteger tailBits = CharsToBits(data, mainCharsCount, tailCharsCount);
if (tailBits >> tailBitsLength != 0)
{
globalBitsLength += 8;
mainBitsLength = globalBitsLength / BlockBitsCount * BlockBitsCount;
tailBitsLength = globalBitsLength - mainBitsLength;
mainCharsCount = mainBitsLength * BlockCharsCount / BlockBitsCount;
tailCharsCount = (tailBitsLength * BlockCharsCount + BlockBitsCount - 1) / BlockBitsCount;
}
int iterationCount = mainCharsCount / BlockCharsCount;


JavaScript реализация



Я решил создать портировать данный алгоритм и на JavaScript. Для работы с большими числами использовалась библиотека jsbn. Для интерфейса использовался bootstrap. Результат можно посмотреть тут: http://ift.tt/1eL3wZv
Заключение



Данный алгоритм написан так, что его легко портировать на другие языки и распараллелить (это есть в C# версии), в том числе и на GPU. Кстати, про модифицирование алгоритма base64 под GPU с использованием C# хорошо написано здесь: Base64 Encoding on a GPU.

Корректность разработанного алгоритма была проверена на алгоритмах base32, base64, base85 (последовательности получившихся символов получались одинаковыми за исключением хвостов).


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Телеприсутствие Tod Bot — cходить за кофе не вставая из-за стола

После неудачного предыдущего поста и вынужденного отсутствия, мы возвращаемся на Хабр и продолжаем освещать проект «Робот Tod Bot». В данном посте хочется рассказать о пополнение функционала робота – реализации телеприсутствия. Теперь управление роботом доступно из любой точки мира. Как это работает и как, по нашему мнению, должен выглядить хороший интерфейс телеприсутсвия – читайте под катом. Ну и, конечно, всеми любимая картинка в эту тему.





Как говорит нам вики, телеприсутствие – это набор технологий, позволяющий получить впечатление нахождения в другом месте, отличного от нашего физического местонахождения. Высшим пилотажем реализации телеприсутствия, на мой взгляд, была бы передача осязания, обоняния, зрения, слуха, вкуса к возможности перемещения в пространстве. Таким образом, смысл технологий заключаются в возможности воздействовать на окружающую среду и получении обратной связи в виде тех ощущений, которые испытывает пользователь, присутствуя в том месте.

Для того, чтобы обеспечить пользователю убедительный эффект присутствия, как минимум, необходимы ряд технологий, которые позволят:


  • Получать и передавать видеопоток

  • Получать и передавать звук

  • Перемещаться в пространстве

  • Возможность манипулировать предметами в удаленной среде




Говоря о нашем роботе, у него телеприсутсвие реализовано посредством веб-интерфейса. У данного подхода есть неоспоримое преимущество – независимость от используемой платформы на стороне клиента.


Видео



Для получения видео на стороне робота мы используем RGB камеру Kinect и дополнительную веб-камеру, в нашем случае это встроенная в ноутбук вебка, но возможно и использование отдельной. Одна — фронтальная, где мы, собственно, и видим все происходящее, другая — под ноги, где видны все предметы, которые потенциально могут нам помешать при управлении роботом. Изображения с камер расположены в веб-интерфейсе удобным и естественным для человека образом. Если мы хотим увидеть что у нас под ногами, то мы опускаем голову вниз, тот же принцип используется и в нашем веб-интерфейсе – мы переводим взгляд на нижний экран.

Обратная видеосвязь осуществляется за счет веб-камеры, расположенной на стороне клиента. Полученная картинка отображается на дисплее робота или на ноутбуке, установленном на мобильную платформу. Передача видеопотока организована через протокол SIP – это аналог протокола Skype, который широко используется как мини-АТС в офисах и на сайтах-сервисах.
Звук



Для передачи звука мы задействуем встроенную в Kinect сетку микрофонов. Эти микрофоны достаточно хорошего качества, что сводит к минимуму возникновение различных шумов. Для передачи аудиопотока используем все тот же SIP.

В дальнейшем мы хотим задействовать микрофоны Kinect по максимуму и реализовать передачу стереозвука. Несомненно, это позволит пользователю получить еще большее ощущение эффекта телеприсутствия.
Перемещение в пространстве



Что же касается перемещения, то есть два варианта удаленного управления роботом: ручное и автономное. Так как наш робот уже умеет самостоятельно ориентироваться в пространстве, то глупо было бы не использовать эту возможность и здесь. Как и человеку, для робота необходимо ориентироваться в пространстве, и для этого он использует карту помещения, предварительно построенную на основе сенсорной информации.

В web-интерфейсе для более удобного ориентирования пользователя в помещении в нижнем левом углу выведена мини-карта, которая пригодится нам для любого режима управления. Мы всегда находимся в центре мини-карты, а сама карта вращается и перемещается соответственно движению роботу. Так мы можем понять, где именно находится робот, а при двойном клике по ней разворачивается глобальная карта.


Ручное управление



Режим настоящего хардкора. В этом режиме мы управляем робот с клавиатуры клавишами стрелок и вольны ехать куда угодно, даже невзирая на препятствия. В итоге получается подобие гоночного симулятора от первого лица, только на другом конце провода реальный робот и реальное окружение. Ко всему прочему, так же доступно управление скоростями в виде ползунков. Первый ползунок отвечает за линейную скорость, а второй — за скорость поворота робота. Не смотря на кажущуюся простоту, по первой управлять таким образом достаточно сложно из-за отсутствия ощущения габаритов.
Автономная навигация



Автономная навигация предполагает сведение роли пользователя к минимуму. Пользователю необходимо и достаточно будет указать одну либо несколько точек назначения, и робот, прокладывая маршрут и объезжая препятствия, самостоятельно будет перемещаться по ним. При этом будет сохраняться как визуальный, так и звуковой контакт. При необходимости, пользователь может прекратить выполнение автономного перемещения и перейти в ручной режим управления. Для автономного режима будет так же актуальна функция следования за собеседником, реализация которой в наших планах.

Не смотря на кажущуюся полноту функционала, все еще есть над чем работать. Мы хотим сделать web-интерфейс своего рода «пультом управления» с индикаторами уровня заряда батареи и прочих жизненно важных для робота датчиков, через который будут доступны настройка и калибровка робота.


Манипулирование предметами в удаленной среде



Функцией манипуляции предметами может похвастать далеко не каждый робот телеприсутствия. Для этого есть ряд причин, одними из которых является дороговизна самих манипуляторов, стоимость которых порой соизмерима со стоимостью самих роботов и сложность программной реализации.

Tod Bot является исключением и рука у него все таки есть. Так что дело осталось за ПО. Самый простой в реализации способ управления манипулятором — это расположение ползунков/скроллов на панели web-интерфейса, отвечающие за каждую степень свободы манипулятора. Вот только есть одно но: схватить что-либо при таком управлении невероятно сложно. Не понаслышке знаю — пытался взять коробку спичек со стола. Потратил на все это порядка 5 минут. При этом вероятность задевания или опрокидывания рядом стоящих предметов стремится к 100%. Можно, конечно, недельку потренироваться и улучшить свои результаты, но в таком случае это не самый простой и удобный способ.

Как мы видим управление манипулятором через web-интерфейс. Данный процесс должен быть максимально автоматизирован, что не требовало бы от пользователя нервов и сил. Пользователь должен ограничиться лишь выбором попавшего в видеопоток объекта, а система сделает все остальное. Поэтому управление рукой мы доверили программному модулю MoveIt, о котором писали ранее. На текущий момент рука уже умеет избегать столкновения с окружающими предметами при перемещении. Интеграцию MoveIt c веб-интерфейсом мы будем производить по достижению удовлетворительных результатов в захвате предметов. Сейчас же у нас есть определенные успехи в этом направлении.
Как мы подружили веб-интерфейс c ROS?



Говоря о программной реализации телеприсутствия, для начала стоит напомнить, что робот Tod Bot управляется под фреймворком ROS. В ROS вся функциональность распределена по программным узлам, общающихся между собой через публикуемые в темы сообщения. Соответственно, любое новое интегрируемое в систему ПО должно быть представлено в качестве такого узла ROS.

Чтобы реализовать функционал телеприсутствия, с одной стороны, нам нужен веб-сервер с возможностью генерации HTML-страниц и обработки POST/GET запросов, с другой же стороны, нам необходимо из ROS получать данные одометрии и навигационной карты и отправлять команды патрулирования помещения и управления перемещением.

Исходя из этих требований, мы решили оформить весь функционал телеприсутствия в виде программного узла ROS, в качестве веб-сервера использовать CherryPy — минималистский веб-фреймворк на Python, а сохранять данные в NoSQL хранилище Redis в простом формате ключ-значение. В качестве SIP клиента использовался HTML5 клиент sipML5, который позволяет совершать аудио/видео звонки напрямую в браузере.

Как это работает вместе? Оператор в веб-браузере через AJAX запросы передает данные на веб-сервер робота. Python-скрипт узла телеприсутствия обрабатывает данные с веб-сервера и рассылает их в другие узлы ROS, которые уже непосредственно исполняют команды на роботе. От узла телеприсутствия на сторону оператора аналогичном образом поступают данные по карте, одометрии и видеопотока с Kinect, которые рендрятся в HTML5 Canvas. Параллельно через sipML5-клиенты оператора и робота транслируется аудио/видео поток. Кстати, качество бесплатных SIP-сервисов для связи SIP-клиентов претензий не вызывает. Единственное, надо имеет довольно широкий канал интернета.

Так же нам хотелось бы услышать и ваше мнение. Как должна выглядеть эталонная система телеприсутствия?


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Первый запуск ракеты SpaceX Falcon-9 с раскладными опорами для посадки прошёл успешно

Вчера, 18 апреля, ракета-носитель Falcon-9, разработанная компанией Илона Маска SpaceX, успешно вывела на орбиту космический грузовик Dragon с грузом для МКС. Из-за различных технических неполадок запуск откладывался несколько раз на протяжении месяца. Однако с точки зрения перспектив развития космонавтики, этот запуск важен прежде всего не самим грузовиком, который доставит на Международную космическую станцию 2200 кг запасов и оборудования, а ракетой-носителем. Впервые ракета была оборудована раскладными опорами для мягкой посадки. В этот раз ракета всё ещё приводнилась в океан — по словам Маска, шансы на успех составляли всего 30-40%, и пока команда проекта не рискнула сажать ракету-носитель на землю.





Главная задача этого запуска — добиться стабилизации и замедления ракеты, необходимых для мягкой посадки. Хотя из-за шторма поисковая команда всё ещё не выудила ракету из океана, последние показания приборов говорят, что ей удалось стабилизировать полёт, и она не разбилась в момент касания поверхности воды — датчики продолжали посылать данные в течение 8 секунд после касания, пока ракета не приняла горизонтальное положение. На высоте 8.5 км скорость ракеты составляла 360 м/сек, и, что особенно важно, она практически не вращалась — во время предыдущего запуска из за неконтролируемого вращения ракеты центробежная сила помешала топливу попасть в двигатели, из-за чего их не удалось повторно запустить перед посадкой.


Ранее компания SpaceX произвела несколько успешных запусков и посадок уменьшенной модели ракеты-носителя Falcon-9, названной Grasshopper (Кузнечик). Уменьшенная модель (сравнимая по размерам с 10-этажным домом) успешно поднималась на высоту почти в километр и возвращалась на стартовую площадку. А вот так будет выглядеть посадка полноразмерной ракеты Falcon-9:



Более подробные данные о состоянии ракеты и параметрах приземления будут доступны, когда поисковые команды поднимут её из воды. Если всё действительно прошло хорошо, ракету можно будет отремонтировать и использовать повторно. Возможно, во время следующего запуска Falcon-9 попробуют посадить уже на твёрдую землю. В случае успешного завершения испытаний, стоимость вывода грузов на орбиту может уменьшиться в несколько раз. Всего в рамках контракта с НАСА SpeceX должна совершить 12 полётов к МКС. Этот запуск был третьим, то есть для обкатки технологии повторного использования ракет-носителей у Маска осталось девять попыток.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Космический «грузовик» Dragon от SpaceX успешно стартовал к МКС уже в третий раз


сегодня в 11:49



На днях на Хабре появилась заметка со ссылкой на трансляцию космического грузовика Dragon от SpaceX. В тот раз, к сожалению, у космического корабля возникли проблемы, так что запуск отложили. На этот раз все прошло хорошо, и Dragon отправился к МКС в 15:25 по времени мыса Канаверал.


Стоит напомнить, что на этот раз космический корабль «под завязку» наполнен необходимым оборудованием, продуктами питания и прочими нужными на МКС вещами. Общая масса полезного груза — 2,5 тонны.



В число грузов входит оборудование для проведения лазерной связи с Землей, о чем публиковалась новость на Хабре. Также астронавтам отправили интересную установку для выращивания овощей и салата латук. Все это будет опробовано в ближайшее время.



В первый же раз SpaceX отправила Dragon два года назад, 22 мая 2012 года. Второй старт состоялся в марте 2013 года. И, наконец, третий старт успешно прошел в этом году. Интересно, наблюдал кто-то из представителей хабрасообщества трансляцию запуска корабля?



Via livestream




Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.