50 State Quarters 齐了

2009-4-13 22:23 | by 2ndboy

  从去年三月份飞赴加州起,一直到上周五刚从 US 回来的同事帮我带回 08 年的最后 2 枚,我的 50 州 25 美分收集计划终于圆满结束。耗时将近一年又一个月:D

  目前我手里齐全的是一套 D 版(这里 D 版不是指盗版,指的是硬币反面有大写字母 D 的版本,D 表示 Denver——丹佛造币厂,P 表示 Philadelphia——费城造币厂),另外还有一套 P 版的差 3 枚(但是其中一些也是 D 版的:(),看来在西海岸收集 P 版的硬币是有些困难。

下面展示下 The United States Mint 50 State Quarters Program 的全景图(写程序从 Mint 网站上抓下来后拼合的):
50 State Quarters

  上周五得到的就是上图最后一排的最后 2 枚,分别是最晚加入美国的阿拉斯加(1959/1/3)和夏威夷(1959/8/21)。

  50 个 25 美分的币值是 $12.5,按今天的中行折算价 6.83 来算,一套 50 State Quarters 合人民币 ¥85.375,但是淘宝上居然有人卖 300 多,真不是一般黑啊!

Discuz! 6.1 用户密码的存放和生成方式

2009-4-12 13:00 | by 2ndboy

  自 Discuz! 6.1 以后,装 Discuz! 就必须安装 UCenter。经过分析,事实上自从跟 UCenter 集成以后,Discuz! 的用户密码就不是存放在 Discuz! 自己的 cdb_members 表里了,而是放在 UCenter 库里的 uc_members 表里。

  Discuz! 目录下的 register.php,其中向 cdb_members 表里插数据时密码的生成方式如下:

  1.  $password = md5( random( 10 ) );
  2.  ?>

只是用一个随机数的 MD5 值填充了用户密码字段,这跟 admin/members.inc.php 里的做法是一样的,根本没有使用用户自己输入的密码(可见即便 hacker 拿到了 Discuz! 库中用户表的数据,也没办法用 MD5 碰撞的方法找出一个可以登录的用户密码)。

  真正的用户密码保存在 UCenter 库的 uc_members 表里,代码在 uc_client/model/user.php 里:

  1.  function add_user($username, $password, $email, $uid = 0)
  2.  {
  3.      $salt = substr(uniqid(rand()), -6);
  4.      $password = md5(md5($password).$salt);
  5.      $sqladd = $uid ? 'uid=\''.intval($uid).'\',' : '';
  6.      //$appid = UC_APPID;
  7.      $this->db->query("INSERT INTO ".UC_DBTABLEPRE."members SET $sqladd username='$username', password='$password', email='$email', regip='".$this->base->onlineip."', regdate='".$this->base->time."', salt='$salt'");
  8.      $uid = $this->db->insert_id();
  9.      $this->db->query("INSERT INTO ".UC_DBTABLEPRE."memberfields SET uid='$uid'");
  10.      return $uid;
  11.  }
  12.  ?>

(UC_DBTABLEPRE 的定义是 define(‘UC_DBTABLEPRE’, ‘`ucenter`.uc_’);)可以看出来,这里的密码生成算法跟生成 UCenter 创始人密码的算法是一样的,都是用一个加扰串结合原始密码的 MD5 再做一次 MD5。

  知道了以上原理,就可以手工对用户名密码做一些处理了。

WMF SDK 心得小记

2009-4-3 20:37 | by 2ndboy

  貌似最近都是在写业余研究的东西,今天也偶尔写写工作上的东西:-D

  最近两周因为项目需要都在研究 server 和 Windows Media Format SDK,今天就记录一下研究 wmvcopy 这个示例程序时如何把 wmvcopy 改写成让 reader 读 WMV 文件,decode 后拿到 raw data,然后再让 writer encode 这些数据得到一个新的 WMV 的方法。

  原始的 wmvcopy 为了仅仅实现原样复制 WMV,所以让 reader 从 WMV 中读出 compressed data,然后交给 writer 写入另外一个 WMV 文件中,它旁路了 reader 的 decode 和 writer 的 encode 过程,这是通过下面步骤实现的:

1) 在对 reader 进行设置时,调 IWMReaderAdvanced::SetReceiveStreamSample() 的时候第二个参数给的是 TRUE,指示 reader 应用程序想要接收 stream sample/compressed sample,这就旁路了 reader 的 decoder

2) 在 WMF SDK 中,compressed sample 是通过 IWMReaderCallbackAdvanced::OnStreamSample() 回调得到的,uncompressed sample 是通过 IWMReaderCallback::OnSample() 回调得到的。由于 wmvcopy 只是原样复制,所以它只把核心处理逻辑放到了 OnStreamSample() 里

3) 在对 writer 进行设置时,调 IWMWriter::SetInputProps() 的时候第二个参数是 NULL,指示 writer 不用为输入数据配置编码器,这就旁路了 writer 的 encoder

4) 在收到 sample 后,wmvcopy 通过调用 IWMWriterAdvanced::WriteStreamSample() 把 compressed sample 写至最终去向,而查阅 WMF SDK 文档可知,向 writer 写入 uncopressed sample 需要调用 IWMWriter::WriteSample()

5) 由于是原样复制 WMV audio/video 内容,所以 wmvcopy 需要尽可能快的数据处理速度,为了做到这一点,在配置 reader 的时候,它启用了 user provided clock。如果想让 reader 以播放速度来取数据,只把 IWMReaderAdvanced::SetUserProvidedClock() 这个调用传的值由 TRUE 改成 FALSE 是不够的,IWMReaderAdvanced::SetUserProvidedClock() 的文档里有这样的话:

User-provided clocks are only supported when the source file is a local file.

This method can fail if the current source does not support user-provided clocks.
To drive a clock, an application must call DeliverTime, and then wait for IWMReaderCallbackAdvanced::OnTime to reach the time specified.

果然在 CWMVCopy::OnStatus() 的 case WMT_STARTED 里发现确实这里调用了 IWMReaderAdvanced::DeliverTime(),把这里相关的代码注释掉,不用管 OnTime(),然后就可以让 reader 按播放速度去 decode sample 了。

6) 由于只是想要原样复制 WMV 内容,所以 wmvcopy 给 writer 设置了跟从原始文件中读出后取得的 peofile 一模一样的 peofile。由于使用的 profile 一致,所以 reader 的 output 的 audio stream index 可以对应 writer input 的 audio stream index,reader output 的 video stream index 也可以对应 writer input 的 video stream index。这从 wmvcopy 对 OnStreamSample() 的处理上也能看出来:

  1. HRESULT
  2.  CWMVCopy::OnStreamSample(
  3.      /* [in] */ WORD wStreamNum,
  4.      /* [in] */ QWORD cnsSampleTime,
  5.      /* [in] */ QWORD cnsSampleDuration,
  6.      /* [in] */ DWORD dwFlags,
  7.      /* [in] */ INSSBuffer __RPC_FAR *pSample,
  8.      /* [in] */ void __RPC_FAR *pvContext)
  9.  {
  10.      ... ...
  11.      hr = m_pWriterAdvanced->WriteStreamSample(
  12.          wStreamNum,
  13.          cnsSampleTime,
  14.          0,
  15.          cnsSampleDuration,
  16.          dwFlags,
  17.          pSample );
  18.      ... ...
  19.  }

OnStreamSample() 的 wStreamNum 参数被直接用于对 WriteStreamSample() 的调用。这就表示——比如 reader output 的第 0 个 stream 是 audio stream,那么 writer input 的第 0 个输入也是用来接收 audio stream 的。

但是对于 writer 使用了跟 reader 不一样的 profile 这种情况,writer 的第 0 个 input 不一定是用来接收 audio stream 的,所以需要应用程序自己去检查。

下面是检查 reader output stream index 和 audio/video 对应情况的示例代码:

  1. IWMProfile *pReaderProfile = NULL;
  2.  pReader->QueryInterface( IID_IWMProfile, (void **)&pReaderProfile );
  3.  
  4.  DWORD dwStreamCount = 0;
  5.  pReaderProfile->GetStreamCount( &dwStreamCount );
  6.  
  7.  IWMStreamConfig *pStreamConfig = NULL;
  8.  for( DWORD i = 0; i < dwStreamCount; i++ )
  9.  {
  10.      m_pReaderProfile->GetStream( i, &pStreamConfig );
  11.  
  12.      WORD wStreamNumber = 0;
  13.      pStreamConfig->GetStreamNumber( &wStreamNumber );
  14.      pReaderAdvanced->SetReceiveStreamSamples( wStreamNumber, FALSE )// we want to receive uncompressed sample
  15.  
  16.      IWMMediaProps *pMediaProperty = NULL;
  17.      pStreamConfig->QueryInterface( IID_IWMMediaProps, (void **)&pMediaProperty );
  18.  
  19.      GUID type;
  20.      pMediaProperty->GetType( &type );
  21.      if( WMMEDIATYPE_Audio == type )
  22.      {
  23.          dwReaderOutputStreamIndex_Audio = i;
  24.      }
  25.      else if( WMMEDIATYPE_Video == type )
  26.      {
  27.          dwReaderOutputStreamIndex_Video = i;
  28.      }
  29.  }

下面是检查 writer input stream index 和 audio/video 对应情况的示例代码:

  1. DWORD dwInputCount = 0;
  2.  pWriter->GetInputCount( &dwInputCount );
  3.  
  4.  IWMInputMediaProps *pInputMediaProps = NULL;
  5.  for( DWORD i = 0; i < dwInputNum; ++i )
  6.  {
  7.      hr = pWriter->GetInputProps( i, &pInputMediaProps );
  8.  
  9.      GUID type;
  10.      pInputMediaProps->GetType( &type );
  11.  
  12.      if( WMMEDIATYPE_Audio == type )
  13.      {
  14.          dwWriterInputIndex_Audio = i;
  15.      }
  16.      else if( WMMEDIATYPE_Video == type )
  17.      {
  18.          dwWriterInputIndex_Video = i;
  19.      }
  20.  }

拿到了对应关系之后,在处理 OnSample() 时就可以以这样:

  1. HRESULT
  2.  CWMVCopy::OnSample(
  3.      /* [in] */ DWORD dwOutputNum,
  4.      /* [in] */ QWORD qwSampleTime,
  5.      /* [in] */ QWORD qwSampleDuration,
  6.      /* [in] */ DWORD dwFlags,
  7.      /* [in] */ INSSBuffer __RPC_FAR *pSample,
  8.      /* [in] */ void __RPC_FAR *pvContext )
  9.  {
  10.      DWORD dwInputIndex = 0;
  11.      if( dwReaderOutputStreamIndex_Audio == dwOutputNum )
  12.      {
  13.          dwInputInput = dwWriterInputIndex_Audio;
  14.      }
  15.      else if( dwReaderOutputStreamIndex_Video == dwOutputNum )
  16.      {
  17.          dwInputIndex = dwWriterInputIndex_Video;
  18.      }
  19.      else
  20.      {
  21.          return( S_OK );
  22.      }
  23.  
  24.      m_pWriter->WriteSample( dwInputIndex, qwSampleTime, dwFlags, pSample );
  25.      return( S_OK );
  26.  }

《Windows Media 编程导向》上讲到对接 reader output 和 writer input 时用的方法是比较 input 和 output 的 connection name,但是我在调试中发现 writer input 的 connection name 始终拿不到,只能拿到 reader output 的(类似“0”,“1”这类字串值),所以我就用了上面的方法。

7) 如果想以不同的参数对输入 WMV 重新进行编码,那么就需要给 writer 设置一个跟 reader 不一样的 profile。这个目标的正规做法是创建一个空 profile,然后给这个 profile 添加几个 stream,然后设置 stream 的格式,但是过程非常复杂。其实还有近路,那就是直接使用 system profile 或者其它现成的 profile。

使用 system profile 的方法是调用 IWMWriter::SetProfileByID(),参数是 system profile 的 GUID,比如 WMProfile_V80_56Video。

profile 也可以保存成 .prx 文件,其实就是一个 profile 的 XML 描述,这就方便了我们直接使用现成的 profile 来设置 writer,下面是把一个存在于磁盘上的 .prx 文件加载,获得一个 profile 实例的代码:

  1. HRESULT
  2.  LoadProfileFromFile(
  3.      /* [in] */  IWMProfileManager  *pManager,
  4.      /* [out] */ IWMProfile        **ppProfile,
  5.      /* [in] */  const char         *pFilename )
  6.  {
  7.      HRESULT hr           = S_OK;
  8.      HANDLE  hFile        = INVALID_HANDLE_VALUE;
  9.      LPWSTR  pProfileData = NULL;
  10.     
  11.      do 
  12.      {
  13.          hFile = CreateFile(
  14.              pFilename,
  15.              GENERIC_READ,
  16.              FILE_SHARE_READ,
  17.              NULL,
  18.              OPEN_EXISTING,
  19.              FILE_ATTRIBUTE_NORMAL,
  20.              NULL );
  21.          if( INVALID_HANDLE_VALUE == hFile )
  22.          {
  23.              hr = HRESULT_FROM_WIN32( GetLastError() );
  24.              break;
  25.          }
  26.          if( FILE_TYPE_DISK != GetFileType( hFile ) )
  27.          {
  28.              hr = NS_E_INVALID_NAME;
  29.              break;
  30.          }
  31.          DWORD dwLength = GetFileSize( hFile, NULL );
  32.          if( -1 == dwLength )
  33.          {
  34.              hr = HRESULT_FROM_WIN32( GetLastError() );
  35.              break;
  36.          }
  37.          pProfileData = (WCHAR *)new BYTE[dwLength + sizeof( WCHAR )];
  38.          if( NULL == pProfileData )
  39.          {
  40.              hr = E_OUTOFMEMORY;
  41.              break;
  42.          }
  43.          memset( pProfileData, 0, dwLength + sizeof( WCHAR ) );
  44.          DWORD dwBytesRead = 0;
  45.          if( !ReadFile( hFile, pProfileData, dwLength, &dwBytesRead, NULL ) )
  46.          {
  47.              hr = HRESULT_FROM_WIN32( GetLastError() );
  48.              break;
  49.          }
  50.          hr = pManager->LoadProfileByData( pProfileData, ppProfile );
  51.          if( FAILED( hr ) )
  52.          {
  53.              break;
  54.          }
  55.      }
  56.      while( false );
  57.     
  58.      if( pProfileData )
  59.          delete []pProfileData;
  60.      if( hFile )
  61.          CloseHandle( hFile );
  62.      return( hr );
  63.  }

至于现成的 .prx,可以使用 WMF SDK 的 wmgenprofile,也可以用《Windows Media 编程导向》随书光盘里的一个小工具 ShowProfile。

HTTP Range

2009-3-29 17:01 | by 2ndboy

  工作关系,两周来一直在研究 HTTP 和 RTSP 等协议,并且实现了一个小型的 HTTP server。其实写 HTTP server 这种事,03 年就干过。那时为了给前公司的某产品配一个 web 方式的管理界面,曾经实现过一个可以完善支持 CGI 的跨平台 mini HTTP server。那时的精力主要集中在跨平台和支持 CGI 上,这一次主要是为了支持多媒体文件的传输,并且要支持 Range。

  所谓 Range,是在 HTTP/1.1(http://www.w3.org/Protocols/rfc2616/rfc2616.html)里新增的一个 header field,也是现在众多号称多线程下载工具(如 FlashGet、迅雷等)实现多线程下载的核心所在。

Range 的规范定义如下:
ranges-specifier = byte-ranges-specifier
byte-ranges-specifier = bytes-unit “=” byte-range-set
byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec )
byte-range-spec = first-byte-pos “-” [last-byte-pos]
first-byte-pos = 1*DIGIT
last-byte-pos = 1*DIGIT
(RFC2616 里充斥着这种形式的定义,一开始觉得挺乱,后来习惯了就发现其实挺科学,挺好理解的:))

值得注意的就是 byte-range-set 是一个 byte-range 的集合,所以在实际请求中完全可能会出现如下这种形式:
Range: bytes=123-567,789-
这个数据区间是个闭合区间,起始值是 0,所以“Range: bytes=0-1”这样一个请求实际上是在请求开头的 2 个字节。

byte-range-spec 里的 last-byte-pos 可以省略,代表从 first-byte-pos 一直请求到结束位置。其实 first-byte-pos 也是可以省略的,只不过就不叫 byte-range-spec 了,而是叫 suffix-byte-range-spec,规范如下:
suffix-byte-range-spec = “-” suffix-length
suffix-length = 1*DIGIT
比如“Range: bytes=-200”,它不是表示请求文件开始位置的 201 个字节,而是表示要请求文件结尾处的 200 个字节。

如果 byte-range-spec 的 last-byte-pos 小于 first-byte-pos,那么这个 Range 请求就是无效请求,server 需要忽略这个 Range 请求,然后回应一个 200 OK,把整个文件发给 client。
如果 byte-range-spec 里的 first-byte-pos 大于文件长度,或者 suffix-byte-range-spec 里的 suffix-length 等于 0,那么这个 Range 请求被认为是不能满足的,server 需要回应一个 416 Requested range not satisfiable。

server 除了要能理解 Range 请求之外,在回应 client 时还要使用 Content-Range 来告诉 client 他到底发送了多少数据,Content-Range 的规范定义如下:
Content-Range = “Content-Range” “:” content-range-spec
content-range-spec = byte-content-range-spec
byte-content-range-spec = bytes-unit SP byte-range-resp-spec “/” ( instance-length | “*” )
byte-range-resp-spec = (first-byte-pos “-” last-byte-pos) | “*”
instance-length = 1*DIGIT

详尽的细枝末节就不在这里记述了,举个例子说明一下,比如某文件的大小是 1000 字节,client 请求这个文件时用了 “Range: bytes=0-500”,那么 server 应该把这个文件开头的 501 个字节发回给 client,同时回应头要有如下内容:
Content-Range: bytes 0-500/1000

Range 请求的一些注意事项:
1) 不支持 Range 请求的 server 要用“Accept-Ranges: none”对 client 表明心意;server 也可以主动告诉 client “Accept-Ranges: bytes”,但是 client 也可以在没有收到这个指示的前提下向 server 发 Range 请求。
2) byte-range-set 中的区间可以是“有洞”的,也可以是部分重叠的
3) 单区间的 byte-range-set 正常回应就可以了,但是多区间 byte-range-set 需要 server 使用 multipart/byterange 来回应

UCenter 创始人的密码修改

2009-3-26 19:48 | by 2ndboy

  UCenter 创始人密码不是写在数据库里的,而是根据放在 UCenter 安装目录下的 data/config.inc.php 里。基本上,创始人密码是这么产生的:

加密后的密码 = md5( md5( 创始人密码 ) + 加扰串 )

下面是一段重新产生创始人密码的 PHP 代码:

  1. <?php
  2.  $UC_FOUNDERSALT = '加扰串';
  3.  $password = '密码';
  4.  
  5.  $UC_FOUNDERPW = md5( md5( $password ) . $UC_FOUNDERSALT );
  6.  
  7.  echo "define( 'UC_FOUNDERSALT', '$UC_FOUNDERSALT' );\n";
  8.  echo "define( 'UC_FOUNDERPW', '$UC_FOUNDERPW' );";
  9.  ?>

用程序运行后产生的结果替换 config.inc.php 里相应的内容就 ok 了。

基于最近好好学习 Python 才能天天向上的原则,再给出一段多余的 Python 实现:

  1. import hashlib
  2.  
  3.  def md5( data ):
  4.      m = hashlib.md5()
  5.      m.update( bytes( data, 'GB2312' ) )
  6.      return( m.hexdigest() )
  7.  
  8.  UC_FOUNDERSALT = '加扰串'
  9.  password = '密码'
  10.  
  11.  print( "define( 'UC_FOUNDERSALT', '%s' );" % UC_FOUNDERSALT )
  12.  print( "define( 'UC_FOUNDERPW', '%s' );" % md5( md5( password ) + UC_FOUNDERSALT ) )