Телефонная база абонентов своими руками
http://raxp.radioliga.com
(специально для Интернетомании)
Настало время поговорить об уязвимости скрипта вывода данных в онлайн - телефонной базе и... варианте создания универсального локального справочника частных абонентов…
Рис. 1. Телефонный справочник абонентов. Режим WEB монитора
С чего все начиналось и зачем это нужно? Сейчас уже никого не удивишь электронными телефонными справочниками, как локальными, так и онлайн. Появились даже такие “монстры-09” по всем городам СНГ, как MegaContacts [1]. Но у всех у них наблюдается несколько общих недостатков:
- закрытость программного кода
- зашифрованность своих баз
- относительно редкое обновление баз по городам и полное отсутствие данных еще по нескольким десяткам населенных пунктов
- для работы с онлайн-базами нужен доступ к Интернет
- как правило, для обновления требуется качать весь дистрибутив, а это порядка нескольких сот мегабайт
Все это, а также обнаруженная ошибка в скрипте онлайн-справочника и отсутствие нормальной локальной базы 09 по родному городу для ПК и сподвигло на создание рассматриваемого ниже универсального справочника абонентов с открытым исходным кодом...
Краткий экскурс…
Что же такое – база данных? Само понятие ”база данных” появилось в 70-х годах прошлого столетия и обозначало любую упорядоченную совокупность данных. Как правило, нескольких файлов, где каждый файл представлял собой отдельную таблицу, содержащую одно или несколько полей базы. И по сей день все базы можно охарактеризовать следующими признаками:
- возможность внесения изменений-обновлений в записи и модификаций в структуре таблиц
- неоднократность использования несколькими пользователями
- минимальная полнота данных для работы (“minimum minimore”)
- оптимизация поисковых запросов (для ускорения пользования)
- возможность защиты от несанкционироанного доступа (парольный доступ)
- независимость от программы-оболочки
- резервирование (создание дубликата базы в случае потери данных при сбое или намеренно)
Предпосылки реализации ПО. Существующие решения
Про уязвимость в PHP сценариях, обслуживающих онлайн-базы уже неоднократно упоминалось в глобальной сети. Найденные уязвимости позволяют взломщику на расстоянии выполнить произвольные SQL команды в базе данных или произвольный код на атакуемом сервере. Естественно, что под уязвимостью следует понимать не только ошибки в коде выполнения, но и элементарный недосмотр админа-програмиста или результаты “социальной инженерии”.
Уязвимость скрипта вы можете увидеть на примере следующего онлайн – справочника*
http://berdyansk.su/phone_private.php.
* аналогично для базы предприятий http://berdyansk.su/phone.php
Она заключается в недоработке алгоритма PHP скрипта выборки данных: если мы в поле поиска введем скажем 1 символ ”а”, то скрипт выдаст нам всю SQL базу с сервера (в архиве [2] вы найдете этот файл <1.htm>, размером около ~50МВ). А нам это и нужно. Но все-же недостаточно, ведь для дальнейшего использования необходимо привести ее в “удобоваримый вид”. Если взглянуть на содержимое файла внутри, то нужные нам поля ”Ф.И.О, Телефон, Адрес” будут выглядеть так:
…
<tr>
<td width="3%"><img src="images/10x10.gif" width="10" height="10"></td>
<td width="32%">Ф.И.О: <img src="images/10x10.gif" width="36" height="10"></td>
<td width="65%"><b>АБАВА Е.А.</b></td>
</tr>
<tr>
<td width="3%"><img src="images/10x10.gif" width="10" height="10"></td>
<td width="32%">Телефон: <img src="images/10x10.gif" width="36" height="10"></td>
<td width="65%"><b>снят</b></td>
</tr>
<tr>
<td width="3%"><img src="images/10x10.gif" width="10" height="10"></td>
<td width="32%">Адрес: <img src="images/10x10.gif" width="36" height="10"></td>
<td width="65%"><b>ул.ВЕРГОНОВА д.47 кв.8</b></td>
</tr>
…
естественно, что этот лишний “мусор” в базе нам никак не нужен. Как от него избавиться? Ведь вручную копировать записи дело неблагодарное. Для решения подобных проблем служат так называемые – ”парсеры”. Эти утилиты или программные модули просто осуществляют выборку нужных данных из общего ”хлама” за несколько секунд.
Поменьше слов - побольше дела…
Исходя из вышеизложенного, определим основные требования к нашему справочнику:
- возможность произвольной выборки по телефону, адресу и расширенный поиск
- возможность сортировки по выбранному полю (телефону, адресу, фамилии…) кликом на заголовке колонки поля
- возможность подключения внешних баз, а также экспорта – импорта своей (локальной базы)
- автономность базы и независимость, например от IDAPI/BDE (Borland Database Engine от Borland)
- малый размер
- возможность позвонить по выбранному номеру через модем
- просмотр WEB камеры города
Вначале определимся со структурой данных в базе. Для построения справочника нам нужны следующие поля: “Телефон, Улица, Дом, Квартира, Ф.И.О. или название организаций”. Так как мы создаем простую базу, то целесообразней разместить все данные в одной таблице:
Таблица- Структура записей в базе
Имя поля |
Назначение |
Телефон |
номер телефона |
Улица |
место жительства |
Дом |
номер дома |
квартира |
номер квартиры |
ФИО/Название |
инициалы абонента/название организации |
По сути, эта структура и есть наша база, к созданию который мы и переходим…
Практика. Разработка ПО и средства отладки
Итак, приступим к основной задаче. Для работы нам понадобиться следующее:
- среда Borland Delphi 5-7 (компиляция и отладка проекта парсера и телефонного справочника)
- база абонентов для работы (входные данные)
Прежде всего, получим входные данные и сформируем базу телефонного справочника с помощью парсера...
“Ищейка берет след”. Парсим базу
Покажем, как реализовать на практике алгоритм парсера. Еще раз обратите внимание на содержимое файла <1.htm>: теги <td width="65%"><b> повторяются перед каждым набором данных, а значит, выделив то, что находится между ними и закрывающим тегом </b>, мы получим необходимую информацию:
Так как в исходной подстроке запись адреса (см. выше) ”ул.ВЕРГОНОВА д.47 кв.8” имеет цельный вид, то необходимо выделить отдельно: улицу, дом, квартиру. Для чего реализуем функцию окна поиcка в подстроке:
подгружаем "сырые данные" из файла <1.htm>
…
res:= tstringlist.Create;
res.LoadFromFile('1.htm');
…
поиск в подстроке
…
function sel(s: string; k: integer): string; //"ул.ВЕРГОНОВА д.47 кв.8"
var i,z,zz,zzz: integer;
okno: string;
begin
result:= '';
//
for i:=1 to length(s)-1 do begin //ищем начало "дом"
okno:= s[i]+s[i+1];
if okno= 'д.' then begin z:= i;break end
end;
for i:=length(s) downto 1 do begin //ищем первый пробел
if s[i]= ' ' then begin zz:= i;break end
end;
zzz:= 0;
for i:=1 to length(s)-1 do begin //ищем "кв"
okno:= s[i]+s[i+1];
if okno= 'в.' then begin zzz:= i;break end
end;
if k=1 then result:= copy(s,4,z-5);
if (k=2)and(zzz<>0) then result:= copy(s,z+2,zz-z-2);
if (k=2)and(zzz=0) then result:= copy(s,z+2,length(s));
if (k=3)and(zzz<>0) then result:= copy(s,zz+4,length(s)-zz+4);
if (k=3)and(zzz=0) then result:= ''
end;
…
Теперь реализуем функцию окна поиска строки:
собственно парсер
…
var i,j,z,k: integer;
s,okno,b1,b2,b3,b4,b5: string; //временные переменные выборки
begin
for i:= 0 to res.Count-1 do begin //перебираем все строки базы
s:= res.Strings[i];
okno := '';
on_bd:= false;
for j:= 1 to length(s) do begin
okno:= copy(s,j,19); //поиск подстроки вида <td width="65%"><b>
if okno = '<td width="65%"><b>' then begin
for z:=j+19 to length(s) do if s[z]='<' then begin k:= z; break end;
//после найденной подстроки ищем закрывающий тег
application.ProcessMessages;
//
inc(gl); //счетчик записей-
if gl=1 then b1:= copy(s,j+19,k-j-19);
if gl=2 then b2:= copy(s,j+19,k-j-19);
if gl=3 then begin //выборка по адресу- поиск в подстроке (см. выше)
b3:= sel(copy(s,j+19,k-j-19),1);
b4:= sel(copy(s,j+19,k-j-19),2);
b5:= sel(copy(s,j+19,k-j-19),3);
gl:=0; on_bd:= true //признак конца формирования 1- записи базы
end
end
end;
//загоняем найденную запись в базу-
if on_bd then begin
//sql- запрос
bdc.Append;
bdc.FieldValues['Телефон']:= b2;
bdc.FieldValues['Улица']:= b3;
bdc.FieldValues['дом']:= b4;
bdc.FieldValues['кв'] := b5;
bdc.FieldValues['ФИО/Название']:= b1;
bdc.Post //обновить-
end;
end;
bdc.SaveToFile('bd.cds') //сохраняем в бинарник
“Для чего нам понадобился BDC…?” – спросите Вы. Это есть не что иное, как компонент ClientDataSet из стандартной библиотеки DataAccess в Delphi. С помощью него мы будем производить добавление записи в нашу базу. Поскольку на выходе парсера (см. рис.2) получаемая база в бинарнике имеет размеры нескольких мегабайт, то для повышения скорости работы и уменьшения торможения при выборках рациональней держать ее прямо в ОЗУ. Что и делает этот компонент. Кроме того, использование ClientDataSet позволяет избавиться от необходимости каждый раз устанавливать BDE на машину, где будет эксплуатироваться база и соответственно реализовать автономное приложение. А используя методы компонента “SaveFile, LoadFromFile” мы получаем возможность экспорта-импорта нашей базы данных как в формате CSV (бинарном), так и широко распространненом сейчас XML, что обеспечивает независимость ПО от источника данных и соответственно придает универсальность оболочке справочника.
Рис. 2. Парсер базы
Управляем базой. Алгоритм выборки
Как правило, для ускорения работы с базой выбирают не все данные, а только те записи, что удолетворяют заданным пользователем критериям. Ведь весь смысл нашей затеи – именно удобный поиск… Эти критерии обеспечить достаточно просто, ведь существует так называемый фильтр, параметры которого можно задавать прямо в вышеупомянутом компоненте ClientDataSet (см. листинг 4) через свойство Filtered:
алгоритм выборки по базе (фильтр)
…
procedure Ttst.bdcFilterRecord(DataSet: TDataSet; var Accept: Boolean);
begin
//функция POS дает "1" в случае совпадающего поискового запроса и
//записи в наборе данных DataSet
if rb1.Checked then //по номеру телефона
accept:= (Pos(t, upcase(Dataset.fieldbyname('Телефон').asstring))>0);
if rb2.Checked then begin //по улице/дому/квартире
//если заполнено поле "улица"
if (adr.Text<>'')and(dom.Text='')and(kom.Text='') then
accept:= (Pos(a, upcase(Dataset.fieldbyname('Улица').asstring))>0);
//если заполнено поле "дом"
if (adr.Text='')and(dom.Text<>'')and(kom.Text='') then
accept:= (Pos(d, upcase(Dataset.fieldbyname('дом').asstring))>0);
//если заполнено поле "кв"
if (adr.Text='')and(dom.Text='')and(kom.Text<>'') then
accept:= (Pos(k, upcase(Dataset.fieldbyname('кв').asstring))>0);
//если заполнено поле "улица, дом"
if (adr.Text<>'')and(dom.Text<>'')and(kom.Text='') then
accept:= (Pos(a, upcase(Dataset.fieldbyname('Улица').asstring))>0)and
(Pos(d, upcase(Dataset.fieldbyname('дом').asstring))>0);
//строгий поиск
if (adr.Text<>'')and(dom.Text<>'')and(kom.Text<>'') then
accept:= (Pos(a, upcase(Dataset.fieldbyname('Улица').asstring))>0)and
(Pos(d, upcase(Dataset.fieldbyname('дом').asstring))>0)and
(Pos(k, upcase(Dataset.fieldbyname('кв').asstring))>0);
end;
//расширенный поиск по любым введенным данным-
if rb3.Checked then //по номеру телефона/улице/дому/квартире/ФИО-Названию
accept:= (Pos(t, upcase(Dataset.fieldbyname('Телефон').asstring))>0)or
(Pos(a, upcase(Dataset.fieldbyname('Улица').asstring))>0)or
(Pos(d, upcase(Dataset.fieldbyname('дом').asstring))>0)or
(Pos(k, upcase(Dataset.fieldbyname('кв').asstring))>0)or
(Pos(f, upcase(Dataset.fieldbyname('ФИО/Название').asstring))>0);
end;
…
активизация выборки
…
bdc.Filtered:= false; //удаляем предыдущий фильтр
bdc.DisableControls; //блокируем набор данных до срабатывания выборки
bdc.Filtered:= true; //включаем фильтр
bdc.EnableControls; //разблокируем набор данных
…
База звонит?
Да-да, именно. Нет ничего проще позвонить по найденному номеру из базы. Для этого достаточно иметь любой подключенный к ТФоП (телефонной сети общего пользования) модем, а также свободный COM порт на вашем ПК и пользователю достаточно сделать двойной клик по найденной записи в таблице результатов. Эта функция реализуется так: выбираем свободный **COM (или виртуальный, если у вас USB модем) порт и посылаем в него следующую AT команду
- в импульсном режиме 'ATDT '+ nomer + ';'+ #13 + #10 + 'ATH1'+ #13 + #10
- в тональном режиме 'ATDP '+ nomer + ';'+ #13 + #10 + 'ATH1'+ #13 + #10
Где nomer - номер телефона в найденной записи. Модем, приняв данную последовательность команд, производит набор номера и поднимает трубку. Для того чтобы положить трубку, достаточно послать команду: 'ATH0'+ #13 + #10.
** сам модуль по работе с COM- портом вы найдете в [3].
Просмотр WEB камеры
В данной возможности уже нет ничего особенного и сложного. И все же такая мелочь привносит элемент интерактивности работы с базой, когда ты можешь увидеть не только безликие телефоны-адреса, но и “движение на улице”. При желании, более подробно ознакомиться с кодом реализации монитора WEB камер вы можете в [4].
Пользовательский интерфейс
Вот собственно и все. Лишь для удобства пользования выделим на форме отдельные панели для каждого типа поиска (см. рис.3), да и добавим сортировку данных по полю, воспользовавшись свойством IndexFieldNames='ваше поле' компонента ClienDataSet. В результает компиляции проекта имеем (см. рис.4, 5):
Рис. 3. Выбор типа поиска
Рис. 4. Определение критериев поиска
Рис. 5. Просмотр результатов
Заключение
Используя полученную оболочку пользователю не составит труда добавить в нее свою базу или оформить ее соответственно собственному вкусу и надобностям. Как пример, телефонный справочник “Запорожье-2007” [6]. Удачного поиска!
Полные исходные тексты, ресурсы проекта парсера и телефонного справочника (файл b09res.zip), а также исполняемый код (файл b09.zip) вы можете загрузить с сайта автора http://raxp.radioliga.com
Сокращенный вариант статьи опубликован в [7]
Ресурсы
Контактная информация:
raxp@mail.zp.ua
31.01.2009
[Переход к списку статей]
|