Fork me on GitHub

2010/01/14

はてな
なんとなくテーブルにデータがINSERTされたらGrowlされる...なんて仕組み作って見ようと思った。それだけ。
sqliteでextensionを作る。growlはWindowsのGNTPにも対応したmattn謹製gntp-sendを使う。
mattn's gntp-send at master - GitHub

command line program that send to growl using GNTP protocol.

http://github.com/mattn/gntp-send
gntp-sendはコマンドラインプログラムだけど、外部からライブラリとしても使える様にしてあります。
#include <stdlib.h>
#include <sqlite3ext.h>
#include <growl.h>

SQLITE_EXTENSION_INIT1
static void growl_func(sqlite3_context *context, int argc, sqlite3_value **argv) {
    if (argc == 1) {
        const char *text  = (const char *)sqlite3_value_text(argv[0]);
        growl("localhost", "sqlite3", "sqlite3-trigger", "database-update", text, NULL, NULL, NULL);
    }
}
__declspec(dllexport) int sqlite3_extension_init(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) {
    SQLITE_EXTENSION_INIT2(api);
    return sqlite3_create_function(db, "growl", 1, SQLITE_UTF8, (void*)db, growl_func, NULL, NULL);
}
こんなコード書いて
# gcc -shared -dll -I c:/sqlite3 -I headers growldb.c lib/libgrowl-static.a -lws2_32 -o growldb.dll
こんな風にコンパイル(Windowsの例)。
あとはテーブルにトリガー張って
sqlite> create table foo(comment text);
sqlite> select load_extension('growldb.dll');
sqlite> create trigger tri_foo
   ...> before
   ...>   insert on foo
   ...> begin
   ...>   select growl(new.comment);
   ...> end;
試してみよう!



sqlite> insert into foo values('hasegawa! xss xss');


sqlite3-growl
xssキター!

ただしinsertする側は必ずload_extension('growldb.dll')しとかないといけないので、oracleの様には行かない。
真面目な話、この方法をうまく使えばsqliteでネットワークレプリケーションとか出来そう。
えっ?誰得?.......知りません!
Posted at 00:17 in ソフトウェア::lang::c | WriteBacks (0)
Tagged as: c, DB, growl, sql, sqlite
Bookmarks: このエントリーのtweets add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip | add to buzzurl add to buzzurl | add to fc2bookmark add to fc2bookmark | add to Yahoo Bookmark add to Yahoo Bookmark | add to Pookmark add to Pookmark

2009/10/07

はてな
以前、Growl For Windows紹介記事gntp-sendという単純なCのプログラム(パスワードハッシュのみサポート)を書いたのですが、github上でpsinnottさんという方がforkしてUDP送信機能を付けてくれました。オープンソースの素晴らしい所ですね。
さらにlibgrowlというライブラリとしても使える様にしたので、アプリケーションに簡単に組み込む事が出来ます。
GNTPプロトコルは今のところWindowsだけしか使えませんが、UDP送信であればアイコンは出ないものの、他のプラットフォームでもGrowl出来る様になっています。現在WindowsとLinuxで動作確認出来ています。
なお、Windowsの場合はGrowlNotifyというDLLエントリポイントをrundll32用にエクスポートしてありますので
C:\>rundll32 growl.dll,GrowlNotify localhost:23053,MyApp,MyNotify,これはすごい,よーわからんけどね!,http://mattn.kaoriya.net/images/logo.png
と実行する事でGrowl For WindowsでGrowlされる様になっています。gntp-sendという付属コマンドもありますがコマンドラインからrundll32経由で使えて便利ですね!
誰得な機能っていわないで!><
Posted at 01:07 in ソフトウェア::lang::c | WriteBacks (0)
Tagged as: c, gntp, growl, udp
Bookmarks: このエントリーのtweets add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip | add to buzzurl add to buzzurl | add to fc2bookmark add to fc2bookmark | add to Yahoo Bookmark add to Yahoo Bookmark | add to Pookmark add to Pookmark

2009/09/01

はてな
以前、Growl For Windows紹介記事gntp-sendという単純なCのプログラム(パスワードハッシュのみサポート)を書きました。その後、GNTPプロトコルの仕様に合わせパスワードハッシュ以外にもAES/DES/3DESな暗号化通信もサポートしたPerlモジュール、Growl::GNTPを書きました。

C/C++言語から暗号モジュールを扱う上でcryptライブラリはライセンス制約がありますが、public domainなCrypto++を使えばな制約もなくなります。
久々boostを触ってみようとリハビリがてらboost::asioとcrypto++を使って、暗号化通信をサポートしたGNTP Growlプログラムを書いてみた。

crypto++には、ハッシュアルゴリズムや暗号アルゴリズムがごった煮で含まれており、フィルタとして使ったりCBCモードで使ったりと、かなり便利になっています。
例えば乱数を得たいのであれば
CryptoPP::SecByteBlock salt(8);
CryptoPP::AutoSeededRandomPool rng;
rng.GenerateBlock(salt.begin(), salt.size());
こんな感じに、またMD5 Digestのhex文字列を取得したいのであれば
CryptoPP::SecByteBlock passtext(CryptoPP::Weak1::MD5::DIGESTSIZE);
CryptoPP::Weak1::MD5 hash;
hash.Update((byte*)password_.c_str(), password_.size());
hash.Update(salt.begin(), salt.size());
hash.Final(passtext);
CryptoPP::SecByteBlock digest(CryptoPP::Weak1::MD5::DIGESTSIZE);
hash.CalculateDigest(digest.begin(), passtext.begin(), passtext.size());
といった感じに。さらにCBCモードで文字列を暗号化したいならば
CryptoPP::CBC_Mode<CryptoPP::DES>::Encryption
  encryptor(passtext.begin(), iv.size(), iv.begin());

std::string cipher_text;
CryptoPP::StringSource(text, true,
  new CryptoPP::StreamTransformationFilter(encryptor,
  new CryptoPP::StringSink(cipher_text)
  ) // StreamTransformationFilter
); // StringSource
こんな風に書くことも出来る。これだけそろえば出来たも同前。
boost::asioの便利なiostreamを使って
asio::ip::tcp::iostream sock(hostname, port);
sock << "こんにちわこんにちわ!";
綺麗な書き方も出来る。ちなみにboost::asioを使えばWindowsやUNIX上での特有なコードが現れる事がないので、同じソースでWindowsやLinuxでもコンパイル出来てしまうので非常にありがたいですね。

以下、全体のソースです。ヘッダファイルになってます。
#ifndef gntp_h
#define gntp_h

#define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1
#include <sstream>
#include <iostream>
#include <string>
#include <cryptopp/osrng.h>
#include <cryptopp/files.h>
#include <cryptopp/hex.h>
#include <cryptopp/md5.h>
#include <cryptopp/des.h>
#include <cryptopp/aes.h>
#include <cryptopp/filters.h>
#include <cryptopp/modes.h>

#include <asio.hpp>

class gntp {
private:
  static inline std::string to_hex(CryptoPP::SecByteBlock& in) {
    std::string out;
    CryptoPP::HexEncoder hex( NULL, true, 2, "" );
    hex.Attach(new CryptoPP::StringSink(out));
    hex.PutMessageEnd(in.begin(), in.size());
    return out;
  }

  void send(const char* method, std::stringstream& stm) {
    asio::ip::tcp::iostream sock(hostname_, port_);
    if (!sock) return;

    // initialize salt and iv
    CryptoPP::SecByteBlock salt(8), iv(8);
    rng.GenerateBlock(salt.begin(), salt.size());
    rng.GenerateBlock(iv.begin(), iv.size());

    if (!password_.empty()) {
      // get digest of password+salt hex encoded
      CryptoPP::SecByteBlock passtext(CryptoPP::Weak1::MD5::DIGESTSIZE);
      CryptoPP::Weak1::MD5 hash;
      hash.Update((byte*)password_.c_str(), password_.size());
      hash.Update(salt.begin(), salt.size());
      hash.Final(passtext);
      CryptoPP::SecByteBlock digest(CryptoPP::Weak1::MD5::DIGESTSIZE);
      hash.CalculateDigest(digest.begin(), passtext.begin(), passtext.size());

      // initialize crypt
      CryptoPP::CBC_Mode<CryptoPP::DES>::Encryption
        encryptor(passtext.begin(), iv.size(), iv.begin());

      std::string cipher_text;
      CryptoPP::StringSource(stm.str(), true,
        new CryptoPP::StreamTransformationFilter(encryptor,
        new CryptoPP::StringSink(cipher_text)
        ) // StreamTransformationFilter
      ); // StringSource

      sock << "GNTP/1.0 "
        << method
        << " DES:" << to_hex(iv)
        << " MD5:" << to_hex(digest) << "." << to_hex(salt)
        << "\r\n"
        << cipher_text << "\r\n\r\n";
    } else {
      sock << "GNTP/1.0 "
        << method
        << " NONE\r\n"
        << stm.str() << "\r\n";
    }

    while (1) {
      std::string line;
      if (!std::getline(sock, line)) {
        break;
      }
      //std::cout << "[" << line << "]" << std::endl;
      if (line.find("GNTP/1.0 -ERROR") == 0)
        throw "failed to register notification";
      if (line == "\r") break;
    }
  }

  std::string application_;
  std::string hostname_;
  std::string port_;
  std::string password_;
  CryptoPP::AutoSeededRandomPool rng;
public:
  gntp(std::string application = "gntp-send", std::string password = "",
      std::string hostname = "localhost", std::string port = "23053") :
    application_(application),
    password_(password),
    hostname_(hostname),
    port_(port) { }

  void regist(const char* name) {
    std::stringstream stm;
    stm << "Application-Name: " << application_ << "\r\n";
    stm << "Notifications-Count: 1\r\n";
    stm << "\r\n";
    stm << "Notification-Name: " << name << "\r\n";
    stm << "Notification-Display-Name: " << name << "\r\n";
    stm << "Notification-Enabled: True\r\n";
    stm << "\r\n";
    send("REGISTER", stm);
  }

  void notify(const char* name, const char* title, const char* text, const char* icon = NULL) {
    std::stringstream stm;
    stm << "Application-Name: " << application_ << "\r\n";
    stm << "Notification-Name: " << name << "\r\n";
    if (icon) stm << "Notification-Icon: " << icon << "\r\n";
    stm << "Notification-Title: " << title << "\r\n";
    stm << "Notification-Text: " << text << "\r\n";
    stm << "\r\n";
    send("NOTIFY", stm);
  }
};

#endif
使い方は非常に簡単。
#include "gntp.h"

int main(void) {
  gntp client("my growl application", "my-password-is-secret");
  client.regist("my-event");
  client.notify("my-event", "タイトル", "本文", "http://mattn.kaoriya.net/images/logo.png");
  return 0;
}
これだけで暗号化通信をサポートしたGNTP Growl通信が出来てしまいます。ちなみにDESな部分をAESに変えればそのままAES暗号通信する事が出来ます。CBC素晴らしい。

本当はboost 1.40.0に含まれる新しい機能を触るつもりだったのですが、まったく触れてもいませんね...苦笑

追記 githubにソースあげておいた。
mattn's gntppp at master - GitHub

GNTP++: gntp client library writen in C++

http://github.com/mattn/gntppp/tree/master

2009/06/18

はてな
WindowsではGrowl For Windowsとそれが使っているプロトコルGNTPにより、Windowsでもアイコンを使ったGrowlアプリケーションの開発が可能になりました。その一つにmiyagawaさんが作ったgithub growlerのGNTP版でもあるyet another github growlerというのも作りました。
これでMac, Windowsでのgithub growlerがある事になるのですが、Linuxにありません。Linuxにはアイコンが表示できてGNTPプロトコルを喋るGrowlシステムがありません。
そこで以前から使っていた、Growlネットワークプロトコル(Growlネットワークプロトコルはアイコンが出せません)をサポートしているmumblesというGrowlシステムを調べて見たところ、内部ではpythonでDBusによるプロセス間通信を行っている事が分かりました。
mumbles-project.org

a plugin driven, modern notification system for Gnome

http://www.mumbles-project.org/
さらにそのDBusインタフェース上ではアイコン表示をサポートしていた為、これは!と思いGitHubのGrowlアプリケーションを作ってみました。
まず、DBusで通信する為のプラグインを作成します。
DBusでメソッドが呼ばれると、MumblesPluginクラスに渡されるMumblesNotifyオブジェクトのalertメソッドを呼び出します。
全体のソースは以下の様になります。
from MumblesPlugin import *
import dbus
import gnomevfs
import os
import urllib

class GithubMumbles(MumblesPlugin):
  plugin_name = 'GithubMumbles'
  dbus_interface = 'com.github.DBus'
  dbus_path = '/com/github/DBus'
  icons = {'github' : 'github.png'}
  __url = None

  def __init__(self, mumbles_notify, session_bus):
    self.signal_config = {
      'Notify': self.Notify,
      'NotifyNum': self.NotifyNum
    }
    MumblesPlugin.__init__(self, mumbles_notify, session_bus)
    self.add_click_handler(self.onClick)

  def NotifyNum(self, num):
    self.__url = 'http://github.com/'
    icon = self.get_icon('github')
    title = 'Github'
    msg = str(num)+' new messages!'
    self.mumbles_notify.alert(self.plugin_name, title, msg, icon)

  def Notify(self, link, author, text):
    self.__url = link
    path = os.path.join(PLUGIN_DIR_USER, 'icons', 'github-%s' % author)
    if os.path.exists(path):
      self.icons[author] = 'github-%s' % author
      icon = self.get_icon(author)
    else:
      icon = self.get_icon('github')
    self.mumbles_notify.alert(self.plugin_name, author, text, icon)

  def onClick(self, widget, event, plugin_name):
    if event.button == 3:
      self.mumbles_notify.close(widget.window)
    else:
      self.open_url(self.__url)

  def open_url(self, url):
    mime_type = gnomevfs.get_mime_type(url)
    application = gnomevfs.mime_get_default_application(mime_type)
    os.system(application[2] + ' "' + url + '" &')
インタフェースはリンク、作者、本文のみとしました。これをegg形式にビルドしてmumblesのpluginフォルダに置くと、上記のインタフェース呼び出しによりGrowlが表示されます。
MumblesPluginにはget_iconメソッドが用意されており、これにはアイコン名称を渡す事になります。実際にはplugin/iconsというフォルダにある名称のファイルが使用されるので、今回の仕組としてはgithubフィードのチェッカースクリプトでアイコンをplugin/iconsフォルダに格納させ、それを使用してプラグイン側が使用するという形になっています。プラグイン側でアイコンを取って来ても良いのですがアイコンをダウンロードしている最中はGrowlが固まってしまう為、今回の様な作りとなっています。
次にチェッカースクリプトですが以下の様なコードになります。
#!/usr/bin/env python

UPDATE_INTERVAL=1000 # 10 minutes
MAX_NOTIFICATIONS = 40
DEBUG = True
##################################################

import os
import sys
import time
import getopt
import rfc822
import calendar
import urllib
import feedparser
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
import gobject
from BeautifulSoup import BeautifulSoup
from pit import Pit

GITHUB_DBUS_INTERFACE = 'com.github.DBus'
GITHUB_DBUS_PATH = '/com/github/DBus'

config = Pit.get('github.com', {'require': {
    'user'  : 'user id on github.com',
    'token' : 'user token on github.com'
    }})

class Usage(Exception):
  def __init__(self, msg=None):
    app = sys.argv[0]
    if msg != 'help':
        self.msg = app+': Invalid options. Try --help for usage details.'
    else:
      self.msg = app+": DBus notifications on new github messages.\n"

class GithubCheck(dbus.service.Object):
  def __init__(self):
    session_bus = dbus.SessionBus()
    bus_name = dbus.service.BusName(GITHUB_DBUS_INTERFACE, bus=session_bus)
    dbus.service.Object.__init__(self, bus_name, GITHUB_DBUS_PATH)

    self.interval = UPDATE_INTERVAL
    self.notifyLimit = MAX_NOTIFICATIONS
    self.debug = DEBUG

    self.lastCheck = None
    self.minInterval = 60000 # 1 minute min refresh interval

    if self.interval < self.minInterval:
      print "Warning: Cannot check github more often than once a minute! Using default of 1 minute."
      self.interval = self.minInterval
    self._check()

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='sss')
  def Notify(self, link, author, text):
    pass

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='i')
  def NotifyNum(self, num):
    pass

  def _check(self):
    if self.debug:
      if self.lastCheck:
        print "checking feed (newer than %s):" %(self.lastCheck)
      else:
        print "checking feed:"
    try:
      items = feedparser.parse("http://github.com/%s.private.atom/?token=%s" % (config['user'], config['token']))['entries']
    except Exception, e:
      items = []

    if self.lastCheck:
      lastCheck = calendar.timegm(time.localtime(calendar.timegm(rfc822.parsedate(self.lastCheck))))
      for item in items:
        if calendar.timegm(item.published_parsed) < lastCheck:
          items.remove(item)

    self.lastCheck = rfc822.formatdate()
    num_notifications = len(items)

    if num_notifications > MAX_NOTIFICATIONS:
      if self.debug:
        print "%s new entries\n" %(num_notifications)
      self.NotifyNum(num_notifications)
    elif num_notifications < 0:
      if self.debug:
        print "no new entries\n"
    else:
      for item in items:
        path = os.path.join(os.path.expanduser('~'), '.mumbles', 'plugins', 'icons', 'github-%s' % item['author'])
        if not os.path.exists(path):
          html = urllib.urlopen('http://github.com/%s' % item['author']).read()
          soup = BeautifulSoup(html)
          img = soup.findAll('div', {'class':'identity'})[0].find('img')['src']
          img = img.replace("?s=50&", "?s=30&");
          urllib.urlretrieve(img, path)
        self.Notify(item['link'], item['author'], item['title'])
        time.sleep(6)
    gobject.timeout_add(self.interval,self._check)

if __name__ == '__main__':
  DBusGMainLoop(set_as_default=True)
  try:
    try:
      opts, args = getopt.getopt(
        sys.argv[1:], "hp", ["help"])
    except getopt.GetoptError:
      raise Usage()

    for o, a in opts:
      if o in ("-h", "--help"):
        raise Usage('help')
      else:
        raise Usage()
  except Usage, err:
    print >> sys.stderr, err.msg
    sys.exit(2)

  t = GithubCheck()
  try:
    loop = gobject.MainLoop()
    loop.run()
  except KeyboardInterrupt:
    print "githubcheck shut down..."
  except Exception, ex:
    print "Exception in githubcheck: %s" %(ex)
だらだらとしたコードですが、大体分かってもらえるかと思います。pitを使っているので初回起動のみユーザとトークンをエディタで入力する必要があります。トークンはGitHubのダッシュボードにあるRSSアイコンのリンク先URLに含まれています。

実行してしばらくすると以下の様な画面が表示されます。
mumbles-github-growler
これで快適になりました。
github上で全てのソースを公開しています。
mattn's mumbles-github-growler at master - GitHub

github growler using mumbles plugin and checker script.

http://github.com/mattn/mumbles-github-growler/tree/master
よろしければどうぞ。