作者归档:Neil

收藏 scp (Secure copy)协议

收藏一篇描述scp协议的文章。 很难找到的。

原文链接:https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works

为了防止原文链接失效, 我把内容粘贴过来。

==================================================分割线=============

How the SCP protocol works

By janp on Jul 09, 2007

Have you ever wondered how the scp and rcp commands worked? The first time I did I haven't found any documentation on the subject. There is no RFC, no draft, not even README file describing it. After reading the source code I tried again and realized that old version of rcp.c might be really the only original documentation available. And since I worked on a couple of bugs in our scp(1) some time ago I put a note in my todo list to write something about it, for the next time I'm going to need it.

A very short history of the protocol

The rcp command appeared in 4.2BSD in 1982, with this manual page. Since then it evolved a little bit which means that old rcp can't work together with current rcp in all cases. The same protocol was used in ssh-1.2.x implementation which was later used as a base for OpenSSH. Since Solaris Secure Shell is a fork of OpenSSH it means that the very same protocol is used in scp(1) in Solaris. Having said all of that I should have probably named this blog entry How the RCP protocol works but it doesn't look cool, you know. If you have more information about the history of the protocol let me know please.

How it works

I will talk only about scp. As already said, rcp is the same beast with regard to the protocol, it just uses rlogin as a mode of transportation. The synopsis for scp(1) is like this:

scp [options] [user@]host1:]file1 []... [ [user@]host2:]file2

In all cases aside from remote-to-remote scenario the scp command processes command line options and then starts an SSH connection to the remote host. Another scp command is run on the remote side through that connection in either source or sink mode. Source mode reads files and sends them over to the other side, sink mode accepts them. Source and sink modes are triggered using -f (from) and -t (to) options, respectively. These options are for internal usage only and aren't documented. There is also the 3rd hidden option, -d, when the target is expected to be a directory.

So, slightly simplified, the local to remote mode of scp works like this:

The protocol

So, how does the transfer protocol actually works? If you forget about ssh, sshd and the connection between them and concentrate only on interaction between scp in "normal" mode and scp in the sink mode, you can see the scenario like this (if you copied from remote to local the remote scp command would have been run with -f option instead of -t, denoting the source mode):

Another important thing is that scp processes with options -f and -t never run against each other. That means that one of those options is always used on the remote side and local scp process (the one started by the user from the command line) then simulates the other mode because it's also the process that interacts with the user.

The source mode

The protocol is a mixture of text and binary data that form protocol messages. For example, when the regular file is about to be sent (= source mode), the type of the message, mode, length and filename are provided in plain text, followed by a new line. The file data itself follows; more on this later. The message can look like this:

C0644 299 group

There might be more protocol text messages before the binary data transfer actually begins. The scp in source mode (= data producer) always waits for a reply before the next protocol line is sent. After the last protocol message was sent, the producer sends a zero byte to notify scp in sink mode about beginning of the actual data transfer. A confirmation zero byte is sent by the sink mode scp process after the last byte of a file was read on the other side.

The sink mode

Every message and every finished file data transfer from the provider must be confirmed by the scp process that runs in a sink mode (= data consumer). The consumer can reply in 3 different messages; binary 0 (OK), 1 (warning) or 2 (fatal error; will end the connection). Messages 1 and 2 can be followed by a text message to be printed on the other side, followed by a new line character. The new line character is mandatory whether the text is empty or not.

List of protocol messages

Cmmmm <length> <filename>
a single file copy, mmmmm is mode. Example: C0644 299 group
Dmmmm <length> <dirname>
start of recursive directory copy. Length is ignored but must be present. Example: D0755 0 docs
E
end of directory (D-E pairs can be nested; that's why we can copy recursively)
T<mtime> 0 <atime> 0
modification and access times when -p options is used (I guess you know why it doesn't make sense to transfer ctime). Times are in seconds, since 00:00:00 UTC, Jan. 1, 1970. Two zeroes are present there in case there is any need to use microseconds in the future. This message was not present in original rcp implementation. Example: T1183828267 0 1183828267 0

After the messages the raw data is transfered. The consumer reads exactly that much data as specified in the length field. D and T message must be specified before any other messages. This is because otherwise it couldn't be clear whether those lines are part of the protocol or part of the data transfer. From the way how the protocol works we can induce that:

 

  • after C message the data is expected (unless the file is empty)
  • after D message either C or E is expected. This means that it's correct to copy an empty directory providing that user used -r option.

Maximum file size and file integrity protection

Maximum file size depends on the SCP software and the systems (and filesystems) where the software is used on. Given the fact that the file size is transferred as text the only limitation may happen in the server or the client. OpenSSH (so SunSSH as well) uses long long int type to process the file size. This type must be able to hold at least 2\^63. That's a huge number since 2\^40 is 1000GB (a thousand GB), for example. This means that practically there is no file size limit in OpenSSH as such. Do not forget that on FAT32, for example, you can not have a file greater than 4GB.

Strong integrity protection is provided by the underlying SSH protocol. Some of that has been discussed in my blog entry on some SSH error messages, full specification of the protocol can be found in RFC 43253, The Secure Shell (SSH) Transport Layer Protocol.

Examples

Now it's time to have some fun. The protocol description might not be that describing like a few simple examples.

  1. single file copy to the remote sidelet's have a file test, containing string "hello" and we want to copy it over SCP protocol to /tmp directory.
    $ rm -f /tmp/test
    <b>$ { echo C0644 6 test; printf "hello\\n"; } | scp -t /tmp</b>
    test                 100% |\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*| 6       00:00    
    $ cat /tmp/test 
    hello

    Nice, isn't it? I used printf so that it's clear why we used 6 for the file length. Now something with a directory copy.

  2. recursive directory copy to the remote sidelet's have the file test in a directory testdir. Now we want to recursively copy the whole directory to /tmpon the "other side".
    $ rm -rf /tmp/testdir
    <b>$ { echo D0755 0 testdir; echo C0644 6 test;
        printf "hello\\n"; echo E; } | scp -rt /tmp</b>
    test                 100% |\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*| 6       00:00    
    $ cat /tmp/testdir/test 
    hello

    Note that we had to use -r option because the directory copy was involved.

  3. copy the directory from the remote sidenow the scp program in the pipe will represent the remote side, the producer of the data. As said in the protocol section, the consumer (we) must reply to every protocol message and also to the finished data transfer. Note that this will not create any directory or file since generated protocol messages and data sent are just printed to your terminal - no one reads or interprets them, we just want to see scp's output in the source mode:
    $ cd /tmp
    $ rm -rf testdir
    $ mkdir testdir
    $ echo hello &gt; testdir/test
    <b>$ printf '\\000\\000\\000\\000\\000\\000' | scp -qprf testdir</b>
    T1183832947 0 1183833773 0
    D0700 0 testdir
    T1183833773 0 1183833762 0
    C0600 6 test
    hello
    E

    A little explanation - you don't see data progress bar because of -q option. You see time protocol messages because we asked for them via -p option. And -f means that scp was the producer of the data. Also note that we had to use six '\\0' characters - the first for initializing the transfer, 4 to confirm the messages and 1 for the data transfer. Is that correct? Not exactly because we didn't acknowledged the final E message:

    $ echo $?
    1

    and that's why scp returned failure. If we use 7 binary zeroes everything is fine then:

    <b>$ printf '\\000\\000\\000\\000\\000\\000\\000' | scp -qprf testdir</b>
    T1183832947 0 1183833956 0
    D0700 0 testdir
    T1183833773 0 1183833956 0
    C0600 6 test
    hello
    E
    $ echo $?
    0
  4. sending an error messageThe example shows that scp will exit when we reply with binary 2. You can see that even when we send a couple of zeroes after that the scp command doesn't accept them anymore.
    <b>$ printf '\\000\\000\\002\\n\\000\\000' | scp -qprf testdir</b>
    T1183895689 0 1183899084 0
    D0700 0 testdir

Running scp with talkative shell profiles on the remote side?

People sometimes hit problems with scp while SSH connections continue to work. This is usually a problem with adding echo/printf commands to their shell profile. See two examples.

scp just hangs after the password is entered when I try to copy to the remote side

For example, this can happen if you add this to your shell profile on the remote system:

echo ""

Why it just hangs? That comes from the way how scp in source mode waits for the confirmation of the first protocol message. If it's not binary 0, it expects that it's a notification of a remote problem and waits for more characters to form an error message until the new line arrives. Since you didn't print another new line after the first one, your local scp just stays in a loop, blocked on read(2). In the meantime, after the shell profile was processed on the remote side, scp in sink mode was started, which also blocks on read(2), waiting for a binary zero denoting the start of the data transfer. So, both scp's are blocked on reading, effectively causing a deadlock. In summary, the problems was caused because your remote shell through its profile output "joined" the protocol conversation.

scp just executes my profile and exits if I copy to the remote side

...meaning that scp just prints the 1st message that is printed from user's shell profile and exits. That's because you added for example this into your shell profile:

$ echo 'echo "hi there!"' >> .bashrc

and then run the scp command:

$ cp /etc/passwd localhost:/tmp
hi there!
$ echo $?
1

This is a very similar problem to the one already mentioned. Since the first character received wasn't binary 0 (but character 'h') it assumes a problem, reads up to the next new line character, prints that out as an error message and exits.

There is an easy fix for those problems - just print what you want when you have a terminal, like this:

tty -s && echo "hi there!"

I see protocol error: unexpected <newline> message and scp exits

Again, similar to the 1st problem, but you are copying from the remote side. What happened? Your local scp, the data consumer, waits for the protocol message from the producer. However, it gets an empty line immediately followed by a new line character which is a violation of the protocol and your local scp then bails out. If you print more characters in your remote shell profile it is considered an error message (unless it starts with a valid protocol character in which situation the message finally printed before it fails will be even more confusing) and the whole message up to the new line character is printed and scp exits then. Example if I add printf "XXXX" to my profile (remember, printf(1) doesn't automatically add a new line) - the whole output up to the first protocol message ending with the new line is considered an error message:

$ scp localhost:/etc/passwd .
Password: 
XXXXC0644 1135 passwd
$ echo $?
1

And if you mess up with a valid message, for example D with printing this from your remote shell profile: printf "D":

$ scp localhost:/etc/passwd .
Password: 
protocol error: bad mode
$ echo $?
1

Moral of this? Always check the return code of scp.

Extensibility of the protocol

The protocol is very simple so the question is how extensible can it be. What if we wanted to transfer ACL information as well? The problem is how to extend it in a backward compatible way. Maybe I'm missing something but I doubt it is possible in an easy way. The problem is that you can't extend existing messages. See what happens when we try to add "123" at the end of T message:

$ { echo T1183832947 0 1183833773 0 123;
    echo D0755 0 testdir; echo E; } | scp -rt /tmp
scp: protocol error: atime.usec not delimited

and similary with C message:

$ { echo D0755 0 testdir; echo C0644 6 test 123;
    printf "hello\\n"; echo E; } | scp -rqt /tmp
$ ls -1 /tmp/testdir/
test 123

You can't add a new message because the scp command refuses it right away:

$ { echo X 1 a; echo D0755 0 testdir; echo C0644 6 test;
    printf "hello\\n"; echo E; } | scp -rt /tmp
scp: X 1 a
$ echo $?
1

One possible way (are there other ways?) I see is that you could start the scp command on the other side with a new option meaning some specific extensions can be used. If it fails it probably means that the scp command is from another vendor and your scp will run it again in a compatible mode. However, I'm not sure this is worth the hassle. Some vendors use SFTP protocol even for scp(1) and that is what we are thinking about, too. I think it might be possible just to exec sftp(1) in non-interactive mode after converting some options. The sftp command can already download files using command line and there is 6474758 for upload.

Remote to remote mode

A common question is why remote to remote copy doesn't work with password or keyboard-interactive authentication. It's not a bug, it's a feature. It could be done in the code but most important reason why not to might be that you wouldn't want to reveal your hostB password to hostA because this is how it works - you local scp runs ssh to hostA with remote command like this: "scp fileX hostB:...".

Recently we updated the scp man page with this section:

Generally, use of scp with password or keyboard-interactive
authentication method and two remote hosts does not work. It does
work with either the pubkey, hostbased or gssapi-keyex
authentication method. For the pubkey authentication method,
either private keys not protected by a passphrase, or an explicit
ssh agent forwarding have to be used. The gssapi-keyex
authentication method works with the kerberos_v5 GSS-API
mechanism, but only if the GSSAPIDelegateCredentials option is
enabled.

Efficiency

You can understand now from the way how scp protocol works why copying many small files over a high latency link might take so long in comparison to tarring the whole directory and pipe it through ssh. Those confirmation messages after every protocol message and data transfer is a big overhead. So the next time, you can do something like this and you will see how faster it can be:

tar cfv - testdir | ssh user@host 'cd /tmp; tar xfv -'

Conclusion

That's all. I would just conclude that the rcp/scp protocol is a very simple file transport protocol that appeared when the rcp command started to be shipped with 4.2BSD. It wasn't designed with extensibility in mind and SFTP protocol might replace it in the future in many scp implementations.

=======================================分割线完=====================

欢迎大家访问我的个人独立博客: http://www.byneil.com

 

 

我眼中的SAML (Security Assertion Markup Language)

提到SAML (Security Assertion Markup Language), 很多人都会联想到单点登录SSO。那么Saml到底是什么,它跟sso到底有什么联系?这里给大家分享一下我在读完了saml差不多全部规范之后的一些心得。希望给saml入门者一些帮助。

Saml是什么

首先,saml是一种xml格式的语言。 翻译过来大概叫 安全断言(标记)语言。  这里有两个点: 第一是“安全”, 第二是“断言(assertion)”。  用人话翻译saml就是 用安全的方式表达断言一种语言。

先看它的核心概念“断言”。  断言是什么?  就是做出判断的语言。比如一句话: 小明是超级管理员。 这就是一个断言。再来一个例子:小红没有权限读取根目录。这也是一个断言。  这种“做出判断的语句”我们在很多场合都需要用到。  比如你在网上尝试登陆一个服务的时候, 这个服务需要知道你是不是合法的用户。 这个时候如果你能提供一个“安全,可靠,可信任”的断言:“小明有权登陆XX服务”, 那么这个服务就知道你合法了, 于是就能为你提供服务了。  这个例子比较抽象,但基本上能表达断言在实际用例中的作用了。 实际上saml的大部分用例就在于证明你是谁,你拥有什么权限等等了。 saml中大部分主要内容也都是类似于:你是谁, 你有什么。。等等这些简单的语句。 详细内容后面会介绍。

接下来第二个概念就是“安全”了。  你能提供一个断言, 别人能不能假冒你提供一个断言从而骗取服务端的信任呢? 另外服务端为什么会信任你给的断言呢? 这就涉及到安全的问题了。为了防止断言被假冒,篡改。saml中加入了安全措施。 当然现今能抵御假冒,篡改,重放攻击的利器就是公钥-私钥系统了。  通过给断言加上签名和加密,再结合数字证书系统就确保了saml不受攻击。

 

在很多sso的场合中, 都支持saml登陆。 这就是saml最多的一个应用场景。  作用相当于大家熟知的OpenID,和Oauth等等。

 

好了,说完了大体的概念,就来程序员最喜欢的硬菜了。

 

从技术的角度看saml。

saml迄今为止有两个广泛应用的标准, Saml 1.1 和Saml 2.0

为了尝鲜,大家先看两个saml的例子, 看个样子即可,不用阅读内容,给你1分钟, 看完赶紧回来接着看这里哦:

http://en.wikipedia.org/wiki/SAML_1.1

http://en.wikipedia.org/wiki/SAML_2.0

 

恩,很好, 你已经知道saml大概长什么样了。   saml1.1和saml2.0 是同一个标准的两个版本, 他们在逻辑概念或者对象结构上大致相当, 只是在一些细节上有所差异。 这两个版本不兼容。 另外1.1比2.0要简单许多。  所以下面在讲逻辑结构的时候一般不区分这两个版本,除非特别说明的地方。

我猜你一定喜欢下面这种图:

这张图取自:  https://www.oasis-open.org/committees/download.php/11511/sstc-saml-tech-overview-2.0-draft-03.pdf

这是saml2.0的一个极其简单的应用场景.  如果你不嫌烦的话,我来解释一下这个图:

图上共有三个角色, 1,SP, 服务提供者。 2, Idp,认证用户并生成断言。 3,就是用户你了, client。

首先, 你(client)是idp的注册用户, 它有你的用户名和密码,它可以认证你就是你。 其次, SP和Idp两者会被各自的域管理员设置为相互信任对方。并且双方都持有对方的公钥。这是配置好的。第三,有一天,你需要访问sp提供的某个服务,但是sp并不认识你,也没有你的用户名和密码因此不能认证你。 于是就发生了上图所示的8个步骤:

1. 你去访问sp的某个受保护资源,比如浏览器打开: http://www.apc.com/resource1.aspx.

2. sp发现你是新来的,没有认证信息。当然不能给你这个页面内容了。 他就会生成一个 saml的认证请求数据包(当然是saml格式的)。把这个请求放在一个html的form的一个隐藏的域中,把这个html form返回给你。 这个form后面有一句javascript自动提交这个form。 二而form的action地址就是 提前配置好的 idp上的一个地址。

saml认证请求的数据包可能是这个样子的:

==========

<samlp:AuthnRequest
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="aaf23196-1773-2113-474a-fe114412ab72"
    Version="2.0"
    IssueInstant="2004-12-05T09:21:59"
    AssertionConsumerServiceIndex="0"
    AttributeConsumingServiceIndex="0">
    <saml:Issuer>https://sp.example.com/SAML2</saml:Issuer>
    <samlp:NameIDPolicy
      AllowCreate="true"
      Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/>
  </samlp:AuthnRequest>

==========

而返回的html from内容大概设这个样子的:它包含了上面的数据包作为其中一个hidden的值。

=============================

<form method="post" action="https://idp.example.org/SAML2/SSO/POST" ...>
    <input type="hidden" name="SAMLRequest" value="<samlp:AuthnRequest>.......... </samlp:authnreques>" />
    ... other input parameter....
    <input type="submit" value="Submit" />

</form>

<javascript>
document.form[0].submit();// 后面紧跟一句类似这样的提交代码.
</javascript>

=============================

这些代码一部分是复制过来的, 有些是我现写的, 大家领会意思即可,不要在意那些细节。

 

3. 上面的form会被javascript自动提交到idp的某个地址。

4. idp也需要认证你, 于是返回给你一个认证的页面, 可能使用用户名密码认证,也可以使用ntlm认证等等一切可以认证你的方式。 因为idp保存有你的用户名和密码。

5. 同上一步,也是认证你的一个过程。

6. idp在认证你之后。觉得你合法, 于是就为你生成一些断言, 证明你是谁,你有什么权限等等。 并用自己的私钥签名。 然后包装成一个response格式,放在form里返回给你。

断言的格式大概如下:

=============

<saml:Assertion
   xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
   xmlns:xs="http://www.w3.org/2001/XMLSchema"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   ID="b07b804c-7c29-ea16-7300-4f3d6f7928ac"
   Version="2.0"
   IssueInstant="2004-12-05T09:22:05">
   <saml:Issuer>https://idp.example.org/SAML2</saml:Issuer>
   <ds:Signature
     xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</ds:Signature>
   <saml:Subject>
..........
   </saml:Subject>
   <saml:Conditions
.........
   </saml:Conditions>
   <saml:AuthnStatement
     AuthnInstant="2004-12-05T09:22:00"
     SessionIndex="b07b804c-7c29-ea16-7300-4f3d6f7928ac">
     <saml:AuthnContext>
       <saml:AuthnContextClassRef>
         urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
      </saml:AuthnContextClassRef>
     </saml:AuthnContext>
   </saml:AuthnStatement>
   <saml:AttributeStatement>
     <saml:Attribute
       xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500"
       x500:Encoding="LDAP"
       NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
       Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1"
       FriendlyName="eduPersonAffiliation">
       <saml:AttributeValue
         xsi:type="xs:string">member</saml:AttributeValue>
       <saml:AttributeValue
         xsi:type="xs:string">staff</saml:AttributeValue>
     </saml:Attribute>
   </saml:AttributeStatement>
 </saml:Assertion>

=============

其中authnstatement认证语句表示你认证成功了。subject表示你是谁。而attributestatement表示你有哪些属性。 还有一个授权语句上面例子中没有。

Response语句大概如下:

============================

 <samlp:Response
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="identifier_2"
    InResponseTo="identifier_1"
    Version="2.0"
    IssueInstant="2004-12-05T09:22:05"
    Destination="https://sp.example.com/SAML2/SSO/POST">
    <saml:Issuer>https://idp.example.org/SAML2</saml:Issuer>
    <samlp:Status>
      <samlp:StatusCode
        Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </samlp:Status>
    <saml:Assertion
      xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
      ID="identifier_3"
      Version="2.0"
      IssueInstant="2004-12-05T09:22:05">
      <saml:Issuer>https://idp.example.org/SAML2</saml:Issuer>
      <!-- a POSTed assertion MUST be signed -->
     ....................
    </saml:Assertion>
  </samlp:Response>

============================

正如上面第2步一样,它也会把response包装在一个form里面返回给你,并自动提交给 sp的某个地址。

===========

 <form method="post" action="https://sp.example.com/SAML2/SSO/POST" ...>
    <input type="hidden" name="SAMLResponse" value="<samlp:Response>.........</samlp:respons>" />
    <input type="hidden" name="RelayState" value="''token''" />
    ...
    <input type="submit" value="Submit" />
  </form>
<javascript>
document.form[0].submit();// 后面紧跟一句类似这样的提交代码.
</javascript>

===========

 

7. 于是就到了第7步, 这个form被javascript自动提交到sp了。

8. sp读到form提交上来的 断言。 并通过idp的公钥验证了断言的签名。 于是信任了断言。 知道你是idp的合法用户了。 所以就最终给你返回了你最初请求的页面了。  http://www.apc.com/resource1.aspx.

 

好了一个最简单的saml用例就讲完了。 你可以看到其中几乎所有的步骤都可以自动完成,用户在第一步访问资源之后,就看到浏览器再自动跳转,自己不需要操作什么,几秒钟过后,资源就访问成功了。

 

到这里, 相信saml在你心目中的形象一定跟家立体了。  如果你还有兴趣就继续往下看吧.

上面是“远观”, 下面我们走近。

先看saml标准的结构:

 

此图出自: https://www.oasis-open.org/committees/download.php/11511/sstc-saml-tech-overview-2.0-draft-03.pdf

saml标准从内到外 可以分为上图的4个层次:

1. Assertion。 断言。 规定了断言的xml结构, 例如:

==============

<saml:Assertion>

..............

</saml:Assertion?

==============

它规定了,这个assertion节点到底该怎么写, 其实就是这个节点的schema。 按照这个规定写出来的assertion别人才能认识。

2. Protocols。协议。它规定了如何请求(samlrequest)和回复(samlresponse )saml消息,其中当然包含assertion的消息。比如:

===============

<samlp:AuthnRequest>

............

</samlp:AuthnRequest>

 

还有:

<samlp:Response>

..............

</samlp:Response>

 

===============

它规定了怎么发送这些请求消息,和回复消息的结构。 这样sp,idp之间才能通信。

 

3. 绑定。  上面两点都是规定了静态结构。 具体这些消息怎么发送呢。 就是用什么协议来承载这些smal消息呢。就是绑定出马了。 最常用的就是http或者soap消息。  把上面的saml消息通过http或者soap消息来传输。 这样sp和idp就能通信了。  saml1.1只支持 http的soap绑定。 而saml2.0支持更多的绑定。 有兴趣自己阅读标准。   这里需要强调的是, 你可能已经想到了,那就是这个绑定其实不重要。 只要saml消息本身是完整的可靠的,下层用什么协议传输不重要。  对。 saml标准规定的绑定只是一种标准实现。  saml的消息可以绑定到任何协议上, 只要sp和idp实现协商好就行了。  这里面应用最广泛的恐怕要算saml的wss绑定了。 用在微软的一系列产品里面。 包括sharepoint online的登陆授权, windows azure登陆,以及windows store的登陆授权等等。  微软自己在ws-trust和ws-secure协议上传输了saml消息。  这恐怕是saml标准以外用的最多的绑定了。

4. Profile, 这个单词我实在不知道翻译成啥好,所以就写原文把。 我个人喜欢把它叫做一套配置,或者叫解决方案。 它规定了某些场景下一整套saml认证的细节和步骤。 比如, 它规定了比较著名的SSO方案。 就是如何用saml实现sso的一整套配置和详细步骤。  概念就是这样。  同上, 上面的绑定都不确定,所以这个profile就更自由了。 你可以使用任何自己定义的profile,只要你们自己协商好就行了。

 

恩好吧, 先到这吧。大体结构已经出来了。 我并不想详细介绍每个xml节点怎么写。大家可以参考标准规范。 看了这篇随笔,相信如果万一哪天你要做saml, 你也不会害怕了。 后续我可能会再分享一下有趣或者有坑的地方。 欢迎大家访问我的个人独立博客交流学习: http://byNeil.com

附上saml协议标准地址:

https://www.oasis-open.org/committees/download.php/3406/oasis-sstc-saml-core-1.1.pdf

http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf

7z文件格式及其源码的分析(五)

这是7z文件格式及其源码的分析系列的第五篇. 上一篇讲到了7z文件压缩流程。最近太忙了,好久没更新,都快忘了写到哪了。:)

这一篇就说说7z文件的尾头的生成方式吧。 上一篇已经讲了尾header的结构了。它其实就是记录了压缩文件详细信息。

那么尾header是如何存储的呢?

先看一个图:

1

这是整个7z文件的结构。  最后面的绿色“尾文件头” 就是我们要说的目标。

7z的尾文件头有两种存储方式。

第一,  最简单的, 就是把尾文件头的内容直接写在后面, 不做任何处理。

这种方式最简单,但是却最不常用。 原因是什么。 我们看上一篇中说到的尾文件头的内容就知道了。 举个简单的例子, 比方说你要压缩大量的文件,比如100个文件吧。 为文件头里面就会有大量的空间用来存储文件名,文件大小,文件时间等等。  通常这些信息很多,但是有个共同特点就是重复信息多。 我们知道,对于这些简单的文本信息,其可压缩性非常强。 换句话说,这些信息的压缩比特别大。  于是, 这就引出了,另一种压缩方式。

第二, 把原始的尾header信息用lzma算法再压缩一次。这样可以显著的减少尾header的大小。尤其是在大量文件的时候。

我们来看一个图:

2

实际怎么生成的呢。 这其实是一个递归过程。

尾文件头压缩的思路就是把原始的尾文件头数据当做一个单独的文件流来进行一次前面的压缩过程。就是重复一次前面的7z的压缩过程。 不过这一次只有一个文件,因此只划分一个Folder. 而且压缩方法是指定的LZMA。也就是说只有一个Coder参与。   当然,原始尾文件头的内容可能有敏感信息。 比如里面的文件名等等信息。因此,7z也提供能力在压缩尾文件头的时候同时加密它。 所以压缩尾文件头的时候如果选择加密头信息,则会加入AES Coder加密。

3

 

所以实际尾header就是这样存储的,上面的 PH, 和HH。

 

用户在压缩7z文件的时候,可以选择是否加密文件, 并且可以同时选择是否加密文件头。

如果用户只加密文件,而不加密文件头。 这样的文件,双击直接用7z打开,可以看到里面的文件结构。文件详细信息,但是不能解压文件出来,除非有密码。

如果同时选择加密文件和文件头。 双击这样的文件,7z会直接提示请输入密码,否则连文件结构都看不见。 原因就在这里。 因为文件的结构信息也被加密了,没有密码,连文件头都解压不开。

这一点必zip文件先进, zip只支持文件内容加密。

暂时就到这吧。欢迎大家访问我的个人独立博客:http://byNeil.com 

下一篇给大家介绍7z如何实现流式压缩和解压的, 以及其他一些7z的trick。

 

zip 压缩文件名的unicode(utf-8)支持

参见:http://www.pkware.com/documents/casestudies/APPNOTE.TXT

        Bit 11: Language encoding flag (EFS).  If this bit is set,
                the filename and comment fields for this file
                MUST be encoded using UTF-8. (see APPENDIX D)
D.1 The ZIP format has historically supported only the original IBM PC character 
encoding set, commonly referred to as IBM Code Page 437.  This limits storing 
file name characters to only those within the original MS-DOS range of values 
and does not properly support file names in other character encodings, or 
languages. To address this limitation, this specification will support the 
following change.

zip 文件头标志位的第11个位表示是否支持unicode文件名.

如果为0, 表示默认的使用 IBM 437 编码,

如果为1, 表示使用unicode文件名, 并且  文件名一定使用的是utf-8编码.

概括起来,  zip格式本来只支持英文 IBM437编码.  后来扩展了这个标记. 表示文件名是用utf-8编码表示的.

并且zip只支持utf-8.

 

Tcp 断开连接,及linger

tcp 的断开过程:

image

 

 

只有 client (主动发起断开的一方) 在进入 time_wait 之后会等待一个时间, 这个时间就是linger time.

1. 如果设置linger 为 0,   则主动断开的一方 直接发送RST给对方,然后自己立即结束.  不会进行4次挥手过程.

2. 默认情况下,linger为1,   主动断开的一方会发送FIN给对方,  然后等待进行完整的 4次挥手.   然后, 主动一方会进入一个linger状态, 持续时间为lingertime,  如果设置lingertime为0, 则使用系统的默认时间, 这个时间一般是 2MSL.   如果指定了 LingerTime, 则使用lingertime.      处于linger状态的socket, 查看状态是 time_wait. 不会进行任何操作. 等lingertime时间到了, 就会自动消失了.

 

 

 

 

 

 

 

如何从snapnames获得域名转移码

在snapnames购买的域名会被系统自动加上转移保护, 官方说只要你选择transfer out域名之后, 转移锁会自动解除, 同时系统把转移码发送到你的邮箱里面:

a

 

然后再保存一下:

b

 

正常情况下, 域名锁已经解除, 并且你的邮箱应该收到转移码了。

但是我这次转移却失败了。收到邮件:

c

This email acknowledges your request to transfer out of Moniker.com the following domains. It also includes the authorization codes your gaining registrar will require you provide to initiate the transfer from their side.

To maximize security, these domains will remain unlocked for only 10 days, after which time your transfer out request will expire and you will need to submit a new request to Moniker.com. If you did not request this transfer out, or if you require assistance, please go to our support center at http://support.Moniker.com

Below are your requested domains and the authorization codes. Please consult the registrar to which you are transferring the domains for instructions on how use these codes.

The Reason Given For Transfer Was: Change Of Ownership

Domain Name,Epp AuthInfo (if applicable)
----------- ----------------------------

Errors:
Domain Name: Error Description
-----------  ----------------------------
XXXXXXXXXX: This domain can not be transferred out due to domain security lock.

说因为安全原因。

于是尝试手动去除安全锁:

d

 

然后:

f

框是灰色的,不能选。 上面也说明了, 安全锁只能在转出时自动解除,不能手动解除。

试了很多次,都不行, 于是找到页面上的客服邮箱写信:

g

没想到,第二天,收到退信, 这个邮箱竟然不存在。

h

 

没想到, 这么坑爹。

怎么办。 这难住了我几天。

最后抱着试一试的态度, 点开support网站, 找到提交问题的地方:

i

j

 

填写了要求转出的信息。

等了一天, 这次终于收到了回复:

k

说已经解除了拍卖锁,  然后我再次transfer out, 果然成功了。

再去转别的域名, 还是转不成功。 于是我再次写信去,语气比较严重, 要他们把我所有的域名都解锁。

好吧, 大家记得以后有问题去support网站提交问题吧,不要直接发邮件了。

 

 

 

 

 

Windows 8.1 设置本地账户的方法

 

刚安装windows 8.1正式版,  安装完成之后, 提示设置账户.  貌似只能设置微软的在线账户, 之前的windows8有个选项可以只设置本地账户.  对于这种"改进", 我只想对微软说两个字: 呵呵.

幸好在网上找到了两个方法可以绕过:

1. 拔掉网线, 后退到上一步,  然后在下一步.  系统检测到你没有联网, 就不会提示设置在线账户了.  直接提示设置本地账户.

2. 如果你不喜欢这么暴力的方法, 还有比较文艺一点的.  在设置在线账户的框里面, 随便输入一个邮箱, 和一个密码,  系统检测不到账户, 就会在后面出现一行小字, 点击就可以创建本地账户了.

 

Have fun.

 

 

 

7z文件格式及其源码的分析(四)

这是7z文件格式及其源码的分析系列的第四篇. 上一篇讲到了7z文件静态结构的尾header部分.这一篇开始,将从7z实际压缩流程开始详细介绍7z文件尾header的详细结构.

一, 第一个概念: coder.

在7z的压缩过程中, 一个非常核心的概念就是coder.  一个coder代表一个算法, 通常是指一个压缩或解压算法(也包括过滤算法和加密算法等). 例如, 在7z中lzma算法就是一个coder,  deflate算法也是一个coder.  7z中用于加密的AES256算法也是一个coder.  

所以概念上讲, 能处理一个文件流的算法就是一个coder.  这个"处理"的概念可以是压缩/解压, 加密等等.

(图1)

通常来讲, 一个coder只能处理一个输入流, 并且只有一个输出流.  比如把一个文件流压缩成一个输出流.  但是, 7z中有的coder可以把一个输入流处理成多个输出流, 反过来也可以把多个流处理成一个流. 比如7z的 BCJ2 coder,  它是一个过滤coder, 可以把一个exe文件过滤成四个输出流.  这样的话, 7z的coder概念得到了扩展.  就是可能同时处理多个输入流, 并且可能输出多个流:

(图2)

这里可以先简单的体验一下压缩的过程了:

1. 简单压缩过程,  把文件流交给lzma coder压缩.

(图3)

2. 多coder串联, 理论上可以串联任何两个coder, 而且串联的级数也是没有限制的, 可以串联任意多级. 当然, 由于熵的存在, 串联过个压缩coder是没有意义的.

这里示例的是最常用的一种方式,就是压缩并且加密.

(图4)

注意上图中的o1, 和 i2.   o1是第一个coder的输出流, i2是第二个coder的输入流. 在实际操作中, 这两个流其实是同一个流, 直接把第一个的输出当做第二个的输入.

解压的过程就是上面的逆过程.

上图就是比较完整的一次压缩过程了.

 

二, 第二个概念 Folder, 不是文件夹.

这里的Folder要特别注意,  它不是我们通常指的文件夹.  它也不是任何物理上存在的东西.  

7z在开始压缩之前, 会把文件分类,  大体上是按文件类型以及文件是否需要加密来分类的.   比如说, 把所有的exe文件分成一类(一个Folder), 或者把所有需要加密的文件分在一起. 等等. 具体分类方法以后再说.  这个分类方法并不重要, 7z的实现用的方法比较简单. 实际上如果要实现7z的压缩器的话, 这个分类方法你说了算. 你可以给每个文件划分成一个Folder.

 我们看一个例子:

(图5)

 在这个例子中, 我们共有5个文件需要压缩. 

1. 首先, 通过一定的分组方法, 我们分成了两个Folder,  第一个Folder包括: a.exe, b.exe 和 c.dll 三个文件.  第二个Folder包括:a.txt 和 b.txt.

2. 对Folder1来说, 它包含三个文件, Folder1就简单的把三个文件串联起来,当做一个大文件, 作为输入流 i1 给Coder1 用. 后面的过程就就是上面的 图4 的内容了.

在7z源码的: \CPP\7zip\Archive\7z\ 这个目录下,有 7zFolderInStream.h 和7zFolderInStream.cpp 专门处理把多个文件串联伪装成一个文件的任务. 从Coder1 的角度看, 它只知道有个文件流 i1, 并不知道这个i1 是一个真实的文件 还是由一个Folder伪装的.

 

实际上, 7z概念上最小的压缩单位不是文件, 而是Folder,  它会先把所有的文件都归到一个相应的Folder中, 然后 让这个Folder作为文件流, 流过若干个Coder.  我们再抽象一下上面的压缩过程:

(图 6)

上图中的字母 'i' 表示输入的意思, 'o' 表示输出. 后面的数字表示序号.   简单解释一下, 这个Folder流最初是作为Coder1 的输入流i1. Coder1 的输出流是o1. 这个o1又作为 i2 输入给Coder2用,  然后又是Coder3.    

值得注意的是最后一个coder的输出流 o3.  它就是压缩的最终输出结果了.  它在7z中叫做一个PackedStream. 就是打包的流.  我们叫做p1吧.  如果有多个Folder, 那每个Folder就会有一个或多个PackedStream.  所以所有文件压缩之后就会有 pn.  这n个packedStream会被按顺序存储在 7z的文件主体, 就是上一篇文章中介绍的第二部分.

 

每个Folder 包含了哪些文件, 每个文件大小等等这些详细信息都存贮在7z的尾文件头中了. 在7zformat.txt中有这一段:

      NumFolders
      Folders[NumFolders]
      {
        NumCoders
        CodersInfo[NumCoders]
        {
          ID
          NumInStreams; //表示这个coder 所接受的输入流的个数, 一般是1个
          NumOutStreams; //表示这个coder的输出流的个数, 一般是1个.
          PropertiesSize //一个int值, 表示后面Properties的字节长度
          Properties[PropertiesSize] // 字节数组, 表示这个coder的一些设置信息, 比如压缩级别, 或者AES加密的IV等等. 
        }
        NumBindPairs  // 表示bindpair 的个数. bindpair表示输入流和输出流的绑定关系.  例如上面的图6中, o1和i2是绑定的, o2和i3是绑定的.
        BindPairsInfo[NumBindPairs] //bindpair的数组, 记录每一个bindpair.
        {
          InIndex;  //这个绑定的输入index,  就是上图中对应的 i后面的序号. (不好意思, 画图的时候没注意,图上下表是从1开始的,但是实际上,你懂的, 都是从0开始的.所有上面图中的下标都要减一.)
          OutIndex; //绑定对应的输出index, 就是对应上图中o后面的序号.  同上. 
        }
        PackedIndices //这表示这个folder最终输出的packstream在所有packstream中的序号.
      }
      UnPackSize[Folders][Folders.NumOutstreams] // 这是一个二位数组, 记录每个Folder对应的输出流的个数.
      CRCs[NumFolders] //这是一个Crc的数组, 没个folder 流的crc, 7z目前没有使用这一个字段. 

稍微解释一下上面的结构:

1. NumFolders, 显示一个int32值, 它记录了7z文件中共有多少个Folder.

2. 后面那是folder数组, 一次排布每个Folder. 每个Folder结构如下:

3. NumCoders, int32值, 记录了这个Folder总共进过了几个coder.

4. 后面就是它的所有Coder的数组, 每个coder的结构:  显示一个coder 的id. 就是coder的唯一标示符. 这个id的定义在: DOC/目录下的 methods.txt. 

5. 更详细的信息, 请看上面代码后面的注释吧.

6. 我再强调一点,我画图的时候没有注意,所以图中的i和o后面的序号都是从1开始的, 实际上,你懂的, 每个存储的序号都是从0开始的, 没有例外. 如果你发现哪里的序号和我说的不一样, 请检查这个.  没有例外, 所有的序号都是从0开始的. 包括以后我可能会画的图. 记住都是从0开始的.

 

7, 再有一点就是, 比如上图中的folder经过了 coder1, coder2 和coder3 这三个coder. 实际才存储这三个coder的时候, 是按逆序存储的, 就是先存Coder3, 然后是coder2, 最后是coder1.  这是为了方便解压.

 

 

上面的图6就是一次比较完整的压缩流程,    解压的流程就是反过来, 先分别构建coder1, coder2 和coder3, 然后逆向流动就最终解压了.

每个Folder都会经过一次完整的压缩过程.

 

好了, 主要的压缩过程和结构已经介绍完了.   下一篇将给大家介绍剩下的文件详细信息的存储方式, 以及最终的Header的生成方式.

最后还是欢迎大家访问我的独立博客: http://byNeil.com 

 

写这么多字, 画图都不容易,  帮顶一下吧, 小伙伴们.

 

 

 

7z文件格式及其源码的分析(三)

上一篇在这里.  这是7z文件格式分析的第三篇, 相信有了前两篇的准备,你已经了解了7z源码的大致结构, 以及如何简单调试7z的源码了. 很多同学是不是迫不及待想要拔去7z的神秘外衣,看看究竟了. 好, 这就带你们一探乾坤. 本文开始,我们详细介绍7z的文件存储结构.

要了解7z的结构,  当然最好从官方的说明开始, 尽管这个说明非常简略, 但它的确是我入门时的救命稻草.

打开源码的 "DOC" 目录.  这里面就是官方所有的文档了. 其中只有二个文档跟结构相关:

1. 7zFormat.txt,   这是我们的主角, 里面介绍了7z文件的大体结构.

2. Methods.txt,  这里面介绍了7z压缩算法id的编码规则, 以后会用到.

 

我们从7zFormat.txt文件开始.

Archive structure
~~~~~~~~~~~~~~~~~ 
SignatureHeader
[PackedStreams]
[PackedStreamsForHeaders]
[
 Header 
 or 
 {
 Packed Header
 HeaderInfo
 }
]

 

上面就是7z文件的总体结构了.  我来稍微解释一下.  上面的代码中, 从波浪线往后开始算.    7z的文件结构基本上分为三部分:

1. 前文件头(就是最前面的header).

2. 压缩数据.

3. 尾文件头(就是放在文件末尾的header).

 

一, 前文件头就是上图中的 "SignatureHeader".  它是32个字节定长的.    前文件头其实记录的信息很少, 它的主要目的是记录尾文件头的位置, 压缩的主要结构都是存在尾文件头中.

它的结构如下:

SignatureHeader
~~~~~~~~~~~~~~~
 BYTE kSignature[6] = {'7', 'z', 0xBC, 0xAF, 0x27, 0x1C};
ArchiveVersion
 {
 BYTE Major; // now = 0
 BYTE Minor; // now = 2
 };
UINT32 StartHeaderCRC;
StartHeader
 {
 REAL_UINT64 NextHeaderOffset
 REAL_UINT64 NextHeaderSize
 UINT32 NextHeaderCRC
 }

 

先是固定的6个字节的值, 前两个字节的值是字母 '7' 和'z' 的ascii值.  后面四个字节是固定的: 0xbc, 0xaf, 0x27, 0x1c

然后是两个字节的版本号, 注意主版本号在前面, 次版本号在后面. 目前的版本号是: 0.2,  注意这是7z文件格式的版本号, 不是7z软件的版本号.

然后是四个字节的 UINT32 的值, (注意, 7z的所有数据都是采用小端在前的存储, 所以要注意这四个字节的实际存储顺序是低位字节在前面, 高位字节在后.  后面的所有数据都是这种结构, 所以以后就不再强调了.  ) .  这4个字节的值是做什么的呢?  先抛开这四个字节本身,   前文件头的32个字节中, 已经用去了 6 + 2 +4 =12 个, 还剩下20个字节.  对了, 这四个字节就是剩下的20个字节的CRC校验值.  具体的CRC算法源码, 在源码中的 "C" 文件夹下的 '7zCrc.c' 和 '7zCrc.h'.

最后这20个字节要一起介绍了.   先是8个字节的UINT64的值, 它记录的是尾文件头(上图中的NextHeader)与前文件头的距离, 这个距离是不算前面这32个字节头的, 也就是抛开前面32个字节开始计数的(解压器通过读取这个值,然后从第33个字节开始直接跳过这个距离, 就可以找到尾文件头了).  然后是8个字节的值, 记录了尾文件头的大小(解压的时候, 通过这个值就能读出尾文件头的长度了).  最后还有4个字节的值, 它也是一个Crc校验值,  是整个尾文件头的校验值.

这里需要注意的是, 上图中用的是 "REAL_UINT64" 这个表达方式, 它的意思就是我们通常理解的占8个字节的UInT64的值(当然是小端存储的啦).  这里用了"real", 真.  那是不是还有"假"的InT64呢. 答案是肯定的.   7z为了兼容压缩大文件(大于4G),这个问题曾一度是zip文件的噩梦,  早期的zip只能压缩小于4G的文件, 并且压缩后的总文件大小也不能超过4G, 后来专门做了标准升级. 好了扯远了.   7z一早设计就考虑到了大文件的问题, 所以很多地方都必须用int64来表达,  这样也会带来一个问题, 就是绝大多数case下, 都不可能超过4G(试问一下,你平时有多少压缩文件超过4G 呢),  所以呢, 就会造成8个字节的int64根本用不上, 多余的字节浪费了. 尤其在小文件压缩的时候,  很影响压缩比.  所以呢, 7z采取了一种巧妙的方法. 就是int64并不是都用8个字节存储, 它用一种简单的编码方式,进行变长存储. 在这个文件中也有描述:

REAL_UINT64 means real UINT64.
UINT64 means real UINT64 encoded with the following scheme:
Size of encoding sequence depends from first byte:
 First_Byte Extra_Bytes Value
 (binary) 
 0xxxxxxx : ( xxxxxxx )
 10xxxxxx BYTE y[1] : ( xxxxxx << (8 * 1)) + y
 110xxxxx BYTE y[2] : ( xxxxx << (8 * 2)) + y
 ...
 1111110x BYTE y[6] : ( x << (8 * 6)) + y
 11111110 BYTE y[7] : y
 11111111 BYTE y[8] : y

 

上面就是编码方式: 就是根据第一个字节的内容来判断后面还有多少个字节.

如果第一个字节的最高位是 0, 那后面就没有字节了. 范围在 0-127.

如果第一个字节的最高两位是 1, 0, 表示它后面还有一个字节.  读取方式是: ( xxxxxx << (8 * 1)) + y

依次类推, 不再详细介绍了.

它的写入方法在: \CPP\7zip\Archive\7z\7zOut.cpp 文件的 第204行:

void COutArchive::WriteNumber(UInt64 value)
{
  Byte firstByte = 0;
  Byte mask = 0x80;
  int i;
  for (i = 0; i < 8; i++)
  {
    if (value < ((UInt64(1) << ( 7  * (i + 1)))))
    {
      firstByte |= Byte(value >> (8 * i));
      break;
    }
    firstByte |= mask;
    mask >>= 1;
  }
  WriteByte(firstByte);
  for (;i > 0; i--)
  {
    WriteByte((Byte)value);
    value >>= 8;
  }
}

 

它的读取方法在: 7zIn.cpp 的第210行:

UInt64 CInByte2::ReadNumber()
{
  if (_pos >= _size)
    ThrowEndOfData();
  Byte firstByte = _buffer[_pos++];
  Byte mask = 0x80;
  UInt64 value = 0;
  for (int i = 0; i < 8; i++)
  {
    if ((firstByte & mask) == 0)
    {
      UInt64 highPart = firstByte & (mask - 1);
      value += (highPart << (i * 8));
      return value;
    }
    if (_pos >= _size)
      ThrowEndOfData();
    value |= ((UInt64)_buffer[_pos++] << (8 * i));
    mask >>= 1;
  }
  return value;
}

 

这里贴出来给大家参考一下.   其实, 后面提到的Uint64如果没有特别说明是8个字节, 那它都是采用这种压缩方式存储的. 但是注意UInt32 无论何时都是占4个字节的, 没有采用压缩.

 

二, 第二部分比较简单, 它会比较大, 简单的说, 它就是文件压缩后的压缩数据存放地点. 结构如下:

[PackedStreams]
[PackedStreamsForHeaders]

简单的说, 7z会把文件压缩成若干个"Pack", 就是包的意思, 这里就是按顺序存储这些pack的. 每个pack的位置和大小信息都会记录在尾header中, 解压的时候就会从这里读出pack,然后解压出来.   这里都是简单的排布压缩后的数据, 所以没有多少细节需要介绍的.

 

三, 真正复杂的主角出场了, 尾文件头,  就是7z中所谓的 nextHeader.

Header structure
~~~~~~~~~~~~~~~~
{
ArchiveProperties
AdditionalStreams
{
PackInfo
{
PackPos
NumPackStreams
Sizes[NumPackStreams]
CRCs[NumPackStreams]
}
CodersInfo
{
NumFolders
Folders[NumFolders]
{
NumCoders
CodersInfo[NumCoders]
{
ID
NumInStreams;
NumOutStreams;
PropertiesSize
Properties[PropertiesSize]
}
NumBindPairs
BindPairsInfo[NumBindPairs]
{
InIndex;
OutIndex;
}
PackedIndices
}
UnPackSize[Folders][Folders.NumOutstreams]
CRCs[NumFolders]
}
SubStreamsInfo
{
NumUnPackStreamsInFolders[NumFolders];
UnPackSizes[]
CRCs[]
}
}
MainStreamsInfo
{
(Same as in AdditionalStreams)
}
FilesInfo
{
NumFiles
Properties[]
{
ID
Size
Data
}
}
}

尾header的结构非常复杂,  里面有很多压缩概念,   如若没有理解压缩过程, 单独的纯字节层面介绍是没有意义的.

我们下一篇开始介绍详细的7z压缩流程,  介绍7z是如何把一系列的文件, 压缩成一个大文件的,  怎样利用压缩算放, 怎样排布文件结构.  同时我们再一边来介绍这个尾header的结构.

希望大家多多支持,  给我动力写下去.

欢迎大家访问我的个人独立博客: http://byNeil.com

 

记得顶啊, 小伙伴们.

 

 

 

 

 

 

 

 

7z文件格式及其源码的分析(二)

这是第二篇, 第一篇在这里: 这一篇开始分析7z的源码结构.

一. 准备工作:

1. 源码下载:

可以从官方中文主页下载:http://sparanoid.com/lab/7z/.  为了方便, 这里直接给出下载链接: http://downloads.sourceforge.net/sevenzip/7z920.tar.bz2 .

2. 工具准备:

源码中给的工程文件都是vc6.0的工程.  作者说他不喜欢新vs的界面. 哎.  不过没关系, 我们使用VS2008也一样可以. 有极少地方需要修改一下.  我们使用VS2008 sp1 作为开发环境.

 

二. HelloWorld:

我们在根目录下新建一个目录"7z", 把源码都解压到这个位置.

我们稍后再详细解释这些目录的意思.   先来一个helloworld, 程序员的最爱.

请直接打开这个路径: 7z\CPP\7zip\Bundles\Alone\

用vs打开其中的 Alone.dsw 文件.   提示要转换工程文件. 点击同意.   然后编译这个工程. 如果不出意外的话, 应该提示你编译成功了.

这个时候, 打开 c:\util\  目录.  里面已经生成7za.exe.    注意, 这里是C盘的绝对路径 :  c:\UTIL\7za.exe

好了, 这个7za.exe  就是一个包含全部7z功能的压缩,解压命令行工具了.

我们可以从命令行进入该目录. 输入 7za.exe 回车. 就能看到它的帮助信息啦.

 

我们先拷贝一个文件到这个目录, 比如拷贝一个test.txt 到c:\util\ 里面去.  我们用命令行来压缩它.

==============

c:\util>7za.exe  a  test.7z  test.txt

==============

这个命令把test.txt  压缩成test.7z.   这两个文件名都是可以带路径的,  为了方便, 我们都拷贝到当前目录了.

恩, 我们再来试试解压.

==============

c:\util>7za.exe x  test.7z  -oout

==============

这个命令可以吧test.7z 解压到当前的out目录下.

试试这两个命令吧, 是不是还不错.

 

三, 目录结构详解:

我们先回到最外层目录:

7z源码基本上是按文件类型分类的.

1.  上面的ASM目录, 其中保存汇编代码.  为了极限的性能, 7z使用了部分汇编代码, crc计算, 和aes加密.  这两点都不是必须的, 实际上, 它们都有c语言的实现. 7z会检测, 如果cpu提供了硬件的aes指令, 就会使用硬件aes汇编指令, 而不会使用自己的aes函数.

2. "C" 目录是7z的核心.   实际上, 7z所有的核心算法都是用c语言实现的. 包括所有的压缩算法, 以及我们的主角7z打包算法.   这些c代码非常强悍, 部分代码可以跨平台编译, 甚至能在嵌入式平台上编译.

(a)  这里面有几个工程文件值得我们注意, 找到这个位置: 7z\C\Util\7z

我们来编译它. 它会在debug目录下生成  7zdec.exe

这是一个最小化的指包含7z解压器的独立exe程序.  有了它就可以解压文件了.

 

(b) 找到这个目录,  7z\C\Util\Lzma

编译之后也会在绝对目录生成.   c:\util\7lzma.exe.  这是一个lzma压缩算法的工具.  只能压缩或解压单个文件. 只包含lzma算法.

这可以用来测试lzma算法.

 

(c) 打开这个目录 7z\C\Util\LzmaLib.

这个工程用来生成 只包含lzma算法的 dll.  以便你的程序调用lzma算法.

 

3. "CPP" 目录是也是7z的重点.

前面说了核心的算法都在C目录下面.  那么CPP目录是做什么的呢.   除了核心的算法之外, 7z还有非常丰富的外围功能.  就是他的文件管理器, 以及右键菜单支持等等. 这些与UI和系统相关的功能都是用c++实现的.     此外, C中的核心算法在cpp目录中都有c++的面向对象的封装.  我们来逐一介绍它的子目录:

a ) "Windows" 目录.

其中包含了与windows集成的功能. 包括菜单集成, 剪贴板支持, 文件io,等等.

 

b) "Common" 目录.

通用的工具类.

 

c ) 我们重点介绍 7zip 目录:

 

(1) "Archive" 目录. 包含各种 archive ("打包") 算法的代码.   因为7z不光支持7z文件,  还支持zip, rar, chm等等其他的打包文件格式.

这里面每个目录就是一种格式.   7z目录当然就是我们的主角7z格式了. ( 这里面的代码是对C文件夹里面的代码的c++封装. )  这里面有一个工程文件.

打开这个工程文件, 然后在工程上面点右键,查看属性:

可以看到, 它的输出路径是: C:\Program Files\7-zip\Formats\7z.dll

这是什么意思呢?   对,  这个工程就是封装7z格式的dll.  这个工程的目的就是给我们演示如何让7z支持我们的自定义格式.   具体怎么实现自定义格式, 可以参考这个工程以及 它周围的其他文件夹格式, 比如:Cab, Chm, 等等. 实现相应的com接口, 然后编译到这个目录下, 7z就能自动调用了.  以后有时间我们再详细介绍这个.

 

(2) 再来看 "Bundles" 目录.

这里面的Alone目录, 我们在前面 写 helloworld 的时候已经见识过了. 它是一个包含全部7z功能的console 的exe.

其中我们再介绍另一个重要的目录,  就是Format7zF.

打开它, 编译,  会提示几个链接错误.  原因是因为作者使用的是vc6. 和我们使用的vs2008一些编译选项不支持了.

分别在这几个汇编文件上点击右键:

找到"CommandLine" 参数, 把其中的参数删除:  -omf -WX -W3,  只保留 -c

 

然后再编译就会成功了.

编译完成之后, 它会在绝对目录下生成一个dll.  C:\Program Files\7-Zip\7z.dll

这个dll是做什么的呢?

它是包含7z全部功能的dll.   第三方的程序可以调用这个dll实现7z的所有功能.   包括7z自己本身的文件管理器 都是通过这种方式调用的.

 

好了, 基本上 7z源码的主要目录都介绍完了.  大家有什么疑问可以留言交流.

我们之后的讲解都将使用: 7z\CPP\7zip\Bundles\Alone 这个目录下的工程配合命令行参数来调试讲解. 因为这是一个包含7z全部功能的console程序, 简单好调试.

 

如果你不想了解7z的文件结构,  只是想在你的程序中集成进处理7z文件的功能. 那么已经足够了.  可以打开上面介绍的相应工程,  把源码集成到你的工程中, 或者直接调用: 7z\CPP\7zip\Bundles\Format7zF 生成的dll. 都是不错的选择.

 

下一节开始, 我们将开始讲解7z的文件格式结构了.  完成以后, 我会把地址更新到这里.

欢迎大家访问我的独立博客交流讨论. http://byNeil.com