传送证书与文件描述符
如要我们要与其他的用户共享我们的Linux主机,那么我们一定会为资源访问权限问题而困扰。在这一章,我们将会了解如何由一个本地套接口获得证书以及如何通过套接口传送文件描述符。这两个重要特性为我们用户的安全访问解决方案提供了一个全新的路径,因为我们要确保我们机器的安全。
这些特性是能过使用套接口的附属数据来提供的。这是一个高级主题,而这超出了初级程序的理解范围。初学者也许希望简单的跳过这一章而直接进入下一章的学习。
而高级用户也许希望仔细研究这一章,因为这里介绍了处理附属数据的问题。在这里我们将重点放在可以研究或实验的实际的例子。
这一章涉及下列主题:
如何向一个本地服务器进程发送一个用户证书
如何接收与解释用户证书
如何向本地主机的另一个进程发送一个文件描述符
如何由本地主机的另一个进程接收一个文件描述符
问题描述
假设在我们的Linux系统上有一个用户,我们信任他来维护我们的Web服务器。为了安全目的,我们的Web服务器并没有使用root帐户权限运行。然而,我们希望我们的Web服务器与通常的Web一样,工作80端口。问题就在于Linux将小于1024的端口都看作权限端口号。这就意味着服务器为了启动需要使用root访问(在此之后不再需要root)。最后我们假设并没有使用inetd守护进程。
尽管我们很欣赏我们的朋友为我们的Web服务器所做的工作,但是也许我们并不希望给予其root访问权限。这可以使得我们晚上可以安然入睡。最后,如果可以避免,我们不希望采取setuid的解决方案。
问题就在于需要一个解决方案可以提供特性:
一个特定的用户在80端口启动Web服务器的能力
程序必须不使用setuid权限位
不使用inetd守护进程
这一章将会通过使用下面的方法来为这个问题提供一个解决方法:
一个简单的套接口服务器
服务器所接收的证书必须毫疑问的标识请求用户
必须将会在80端口创建并绑定一个套接口,然后将其发送回已认证的请求用户进程
在一些初始的理论以后,这一章余下的部分将会通过实际的动手演示来展示其如何工作。
简介附属数据
尽管通过网络验证一个远程用户标识是非常困难的,但是在同一个主机验证另一个用户对于Linux内核而言却是一件很简单的事情。这就使得PF_LOCAL/PF_UNIX为另一个端的接收端提供关于用户的证书成为可能。这些证书和解的唯一方法也许就在于内核本身会以一定的方式和解(也许是通过一个内核可装载模块)。
证书可以作为通信中所接收的附属数据的一部分而接收。附属数据对于通常数据来说是补充或是从属。这就引出需要在这里强调的几点问题:
证书是作为附属数据的一部分而接收的。
附属数据必须是补充通常数据的(他不可以独立传送)。
附属数据也可以包含其他的信息,例如文件描述符。
附属数据可以同时包含多个附属项目(例如同时包含证书与文件描述符)。
证书是由Linux内核提供的。他们从来不由客户程序提供。如果是这样,客户端就会被允许 个标识。因为内核是可信任的,证书就可以被对证书感兴趣的进程所信任。
现在我们已经了解文件描述符是作为附属数据来传送和接收的。然而,在我们开始编写套接口代码来使用这些附属数据元素时,我们需要介绍一些新的编程概念。
简介I/O向量
在我们了解使用附属数据工作的复杂函数之前,我们应该熟悉被readv(2)与writev(2)系统调用所使用的I/O向量。我们不仅将会发现这些函数是十分有用的,而他们的工作方式也被引入了一些附属数据函数中。这会使得后面的理解更为容易。
I/O向量(struct iovec)
readv(2)与writev(2)函数都使用一个I/O向量的概念。这是由所包含的文件定义的:
#include <sys/uio.h>
sys/uio.h头文件定义了struct iovc,其定义如下:
struct iovec {
ptr_t iov_base; /* Starting address */
size_t iov_len; /* Length in bytes */
};
struct iovec定义了一个向量元素。通常,这个结构用作一个多元素的数组。对于每一个传输的元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是readv所接收的数据或是writev将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度。
readv(2)与writev(2)函数
这些函数是作为read与write函数的衍生函数而被人所知的。他们以这样的方式进行设计是因为他们可以在一个原子操作中读取或是写入多个缓冲区。这些函数的原型如下:
#include <sys/uio.h>
int readv(int fd, const struct iovec *vector, int count);
int writev(int fd, const struct iovec *vector, int count);
这些函数需要三个参数:
要在其上进行读或是写的文件描述符fd
读或写所用的I/O向量(vector)
要使用的向量元素个数(count)
这些函数的返回值是readv所读取的字节数或是writev所写入的字节数。如果有错误发生,就会返回-1,而errno存有错误代码。注意,也其他I/O函数类似,可以返回错误码EINTR来表明他被一个信号所中断。
使用writev的例子
下面的程序代码展示了如何使用writev函数将三个独立的C字符串作为一次写操作写入标准输出。
/*
* writev.c
*
* Short writev(2) demo:
*/
#include <sys/uio.h>
int main(int argc,char **argv)
{
static char part2[] = "THIS IS FROM WRITEV";
static char part3[] = "]/n";
static char part1[] = "[";
struct iovec iov[3];
iov[0].iov_base = part1;
iov[0].iov_len = strlen(part1);
iov[1].iov_base = part2;
iov[1].iov_len = strlen(part2);
iov[2].iov_base = part3;
iov[2].iov_len = strlen(part3);
writev(1,iov,3);
return 0;
}
编译运行程序:
$ make writev
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type writev.c
gcc writev.o -o writev
$ ./writev
[THIS IS FROM WRITEV]
$
当程序运行时,我们可以看到无论所引用的缓冲区是如何分散,所有的缓冲区都会被输出形成最终的字符串。
也许我们希望多花一些时间来修改这个程序并做各种测试,但是要注意一定要将iov[[]数组分配得足够大。
sendmsg(2)与recvmsg(2)函数
这些函数为程序提供了一些其他的套接口I/O接口所不具备的高级特性。下面的内容我们将会先来看一下sendmsg来介绍这些主题。然后将会完整的介绍recvmsg函数,因为他们的函数接口是相似的。接下来,将会描述msghdr的完整结构。
sendmsg(2)函数
现在是时候进入这个大同盟了。从概念上说,sendmsg函数是所有写入函数的基础,而他是从属于套接口的。下面的列表以复杂增加的顺序列出了所有与入函数。在每一个层次上,同时列出了所增加的特性。
函数 增加的特性
write 最简单的套接口写入函数
send 增加了flags标记
sendto 增加了套接口地址与套接口长度参数
writev 没有标记与套接口地址,但是具有分散写入的能力
sendmsg 增加标记,套接口地址与长度,分散写入以及附属数据的能力
sendmsg(2)函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>
int sendmsg(int s, const struct msghdr *msg, unsigned int flags);
函数参数描述如下:
要在其上发送消息的套接口s
信息头结构指针msg,这会控制函数调用的功能
可选的标记位参数flags。这与send或是sendto函数调用的标记参数相同。
函数的返回值为实际发送的字节数。否则,返回-1表明发生了错误,而errno表明错误原因。
recvmsg(2)函数
recvmsg是与sendmsg函数相对的函数。这个函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>
int recvmsg(int s, struct msghdr *msg, unsigned int flags);
函数参数如下:
要在其上接收信息的套接口s
信息头结构指针msg,这会控制函数调用的操作。
可选标记位参数flags。这与recv或是recvfrom函数调用的标记参数相同。
这个函数的返回值为实际接收的字节数。否则,返回-1表明发生了错误,而errno表明错误原因。
理解struct msghdr
当我第一次看到他时,他看上去似乎是一个需要创建的巨大的结构。但是不要怕。其结构定义如下:
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};
结构成员可以分为四组。他们是:
套接口地址成员msg_name与msg_namelen。
I/O向量引用msg_iov与msg_iovlen。
附属数据缓冲区成员msg_control与msg_controllen。
接收信息标记位msg_flags。
在我们将这个结构分为上面的几类以后,结构看起来就不那样巨大了。
成员msg_name与msg_namelen
这些成员只有当我们的套接口是一个数据报套接口时才需要。msg_name成员指向我们要发送或是接收信息的套接口地址。成员msg_namelen指明了这个套接口地址的长度。
当调用recvmsg时,msg_name会指向一个将要接收的地址的接收区域。当调用sendmsg时,这会指向一个数据报将要发送到的目的地址。
注意,msg_name定义为一个(void *)数据类型。我们并不需要将我们的套接口地址转换为(struct sockaddr *)。
成员msg_iov与msg_iovlen
这些成员指定了我们的I/O向量数组的位置以及他包含多少项。msg_iov成员指向一个struct iovec数组。我们将会回忆起I/O向量指向我们的缓冲区。成员msg_iov指明了在我们的I/O向量数组中有多少元素。
成员msg_control与msg_controllen
这些成员指向了我们附属数据缓冲区并且表明了缓冲区大小。msg_control指向附属数据缓冲区,而msg_controllen指明了缓冲区大小。
成员msg_flags
当使用recvmsg时,这个成员用于接收特定的标记位(他并不用于sendmsg)。在这个位置可以接收的标记位如下表所示:
标记位 描述
MSG_EOR 当接收到记录结尾时会设置这一位。这通常对于SOCK_SEQPACKET套接口类型十分有用。
MSG_TRUNC 这个标记位表明数据的结尾被截短,因为接收缓冲区太小不足以接收全部的数据。
MSG_CTRUNC 这个标记位表明某些控制数据(附属数据)被截短,因为缓冲区太小。
MSG_OOB 这个标记位表明接收了带外数据。
MSG_ERRQUEUE 这个标记位表明没有接收到数据,但是返回一个扩展错误。
我们可以在recvmsg(2)与sendmsg(2)的man手册页中查看更多的信息。
附属数据结构与宏
recvmsg与sendmsg函数允许程序发送或是接收附属数据。然而,这些额外的信息受限于一定的格式规则。这一节将会介绍控制信息头与程序将会用来管理这些信息的宏。
简介struct cmsghdr结构
附属信息可以包括0,1,或是更多的单独附属数据对象。在每一个对象之前都有一个struct cmsghdr结构。头部之后是填充字节,然后是对象本身。最后,附属数据对象之后,下一个cmsghdr之前也许要有更多的填充字节。在这一章,我们将要关注的附属数据对象是文件描述符与证书结构。
下图显示了一个包含附属数据的缓冲区是如何组织的。
我们需要注意以下几点:
cmsg_len与CMSG_LEN()宏值所显示的长度相同。
CMSG_SPACE()宏可以计算一个附属数据对象的所必需的空白。
msg_controllen是CMSG_SPACE()长度之后,并且为每一个附属数据对象进行计算。
控制信息头部本身由下面的C结构定义:
struct cmsghdr {
socklen_t cmsg_len;
int cmsg_level;
int cmsg_type;
/* u_char cmsg_data[]; */
};
其成员描述如下:
成员 描述
cmsg_len 附属数据的字节计数,这包含结构头的尺寸。这个值是由CMSG_LEN()宏计算的。
cmsg_level 这个值表明了原始的协议级别(例如,SOL_SOCKET)。
cmsg_type 这个值表明了控制信息类型(例如,SCM_RIGHTS)。
cmsg_data 这个成员并不实际存在。他用来指明实际的额外附属数据所在的位置。
这一章所用的例子程序只使用SOL_SOCKET的cmsg_level值。这一章我们感兴趣的控制信息类型如下(cmsg_level=SOL_SOCKET):
cmsg_level 描述
SCM_RIGHTS 附属数据对象是一个文件描述符
SCM_CREDENTIALS 附属数据对象是一个包含证书信息的结构
简介cmsg(3)宏
由于附属数据结构的复杂性,Linux系统提供了一系列的C宏来简化我们的工作。另外,这些宏可以在不同的UNIX平台之间进行移植,并且采取了一些措施来防止将来的改变。这些宏是由cmsg(3)的man手册页来进行描述的,其概要如下:
#include <sys/socket.h>
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
size_t CMSG_ALIGN(size_t length);
size_t CMSG_SPACE(size_t length);
size_t CMSG_LEN(size_t length);
void *CMSG_DATA(struct cmsghdr *cmsg);
CMSG_LEN()宏
这个宏接受我们希望放置在附属数据缓冲区中的对象尺寸作为输入参数。如果我们回顾一个我们前面的介绍,我们就会发现这个宏会计算cmsghdr头结构加上所需要的填充字符的字节长度。这个值用来设置cmsghdr对象的cmsg_len成员。
下面的例子演示了如果附属数据是一个文件描述符,我们应如何来计算cmsg_len成员的值:
int fd; /* File descriptor */
printf("cmsg_len = %d/n",CMSG_LEN(sizeof fd));
CMSG_SPACE()宏
这个宏用来计算附属数据以及其头部所需的总空白。尽管CMSG_LEN()宏计算了一个相似的长度,CMSG_LEN()值并不包括可能的结尾的填充字符。CMSG_SPACE()宏对于确定所需的缓冲区尺寸是十分有用的,如下面的示例代码所示:
int fd; /* File Descriptor */
char abuf[CMSG_SPACE(sizeof fd)];
这个例子在abuf[]中声明了足够的缓冲区空间来存放头部,填充字节以及附属数据本身,和最后的填充字节。如果在缓冲区中有多个附属数据对象,一定要同时添加多个CMSG_SPACE()宏调用来得到所需的总空间。
CMSG_DATA()宏
这个宏接受一个指向cmsghdr结构的指针。返回的指针值指向跟随在头部以及填充字节之后的附属数据的第一个字节(如果存在)。如果指针mptr指向一个描述文件描述符的可用的附属数据信息头部,这个文件描述符可以用下面的代码来得到:
struct cmsgptr *mptr;
int fd; /* File Descriptor */
. . .
fd = *(int *)CMSG_DATA(mptr);
CMSG_ALIGN()宏
这是一个Linux扩展宏,而不是Posix.1g标准的一部分。指定一个字节长度作为输入,这个宏会计算一个新的长度,这个新长度包括为了维护对齐所需要的额外的填充字节。
CMSG_FIRSTHDR()宏
这个宏用于返回一个指向附属数据缓冲区内的第一个附属对象的struct cmsghdr指针。输入值为是指向struct msghdr结构的指针(不要与struct cmsghdr相混淆)。这个宏会估计msghdr的成员msg_control与msg_controllen来确定在缓冲区中是否存在附属对象。然后,他会计算返回的指针。
如果不存在附属数据对象则返回的指针值为NULL。否则,这个指针会指向存在的第一个struct cmsghdr。这个宏用在一个for循环的开始处,来开始在附属数据对象中遍历。
CMSG_NXTHDR()宏
这个用于返回下一个附属数据对象的struct cmsghdr指针。这个宏会接受两个输入参数:
指向struct msghdr结构的指针
指向当前struct cmsghdr的指针
如果没有下一个附属数据对象,这个宏就会返回NULL。
遍历附属数据
当接收到一个附属数据时,我们可以使用CMSG_FIRSTHDR()与CMSG_NXTHDR()宏来在附属数据对象中进行遍历。下面的示例代码显示了for循环的通常格式以及宏的相应用法:
struct msghdr msgh; /* Message Hdr */
struct cmsghdr *cmsg;0 /*Ptr to ancillary hdr */
int *fd_ptr; /* Ptr to file descript.*/
int received_fd; /* The file descriptor */
for ( cmsg=CMSG_FIRSTHDR(&msgh); cmsg!=NULL; cmsg=CMSG_NXTHDR(&msgh,cmsg) ) {
if ( cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS ) {
fd_ptr = (int *) CMSG_DATA(cmsg);
received_fd = *fd_ptr;
break;
}
}
if ( cmsg == NULL ) {
/* Error: No file descriptor recv'd */
}
创建附属数据
要发送一个文件描述符的进程必须使用正确的格式化数据来创建一个附属数据缓冲区。下面的代码展示的通常的创建过程:
struct msghdr msg; /* Message header */
struct cmsghdr *cmsg; /* Ptr to ancillary hdr */
int fd; /* File descriptor to send */
char buf[CMSG_SPACE(sizeof fd)]; /* Anc. buf */
int *fd_ptr; /* Ptr to file descriptor */
msg.msg_control = buf;
msg.msg_controllen = sizeof buf;
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof fd);
/* Initialize the payload: */
fd_ptr = (int *)CMSG_DATA(cmsg);
*fd_ptr = fd;
/*
* Sum of the length of all control
* messages in the buffer:
*/
msg.msg_controllen = cmsg->cmsg_len;
提供一个附属数据例子
现在,为了将所有介绍的概念集中到一个例子程序中,我们将会提供两个例子程序:
一个套接口服务器
一个非常简单的Web服务器
Web服务器仅可以服务器一个简单的示例HTML页面。套接口服务器将会用于访问80端口,而不需要root权限。
下面的部分将会提供各种源代码组件。
通常的头文件common.h
这个文件只是简单的列出了所有的函数原型与其他的通常定义和需要包含的头文件。
/*
* common.h
*
* Source common to all modules:
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/un.h>
#include <sys/uio.h>
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif
extern void bail(const char *on_what);
extern int recv_fd(int s);
extern int reqport(int port);
extern int send_fd(
int s,int fd,
void *addr,socklen_t alen);
extern int recv_cred(
int s,struct ucred *credp,
void *buf,unsigned bufsiz,
void *addr,socklen_t *alen);
misc.c模块
这个模块列出在两个程序中通常都会用到的一些小的错误处理函数。这个自说明的模块如下:
/*
* misc.c:
*
* Misc. Functions:
*/
#include "common.h"
/*
* This function report the error to
* the log file and calls exit(1).
*/
void bail(const char *on_what)
{
if(errno!=0)
fprintf(stderr,"%s: ",strerror(errno));
fprintf(stderr,"%s/n",on_what);
exit(1);
}
recvcred.c模块
下面给出我们的示例程序会用到的recvcred.c源码模块。recv_cred()函数集中了接收数据与证书的大部分工作。
/*
* recvcred.c
*
* Send a file descriptor:
*/
#include "common.h"
/*
* Receive Data & Credentials:
*
* Arguments:
* s Socket to read form
* credp Ptr to receiving area for cred.
* buf Ptr to receiving buffer for data
* bufsiz Maximum # of bytes for buffer
* addr Ptr to buffer to receive peer address (or NULL)
* alen Ptr to Maximum byte length (updated with actual length upon retur .)
*
* Returns:
* >=0 Data bytes read
* -1 Failed: check errno
*
*
* Notes:
* The value -1 is returned with errno set
* to ENOENT,if data is returned without
* and credentials.
*/
int recv_cred(
int s, /* Socket */
struct ucred *credp, /* Credential buffer */
void *buf, /* Receiving Data buffer */
unsigned bufsiz, /* Recv. Data buf size */
void *addr, /* Received Peer address */
socklen_t *alen) /* Ptr to addr length */
{
int z;
struct msghdr msgh; /* Message header */
struct iovec iov[1]; /* I/O vector */
struct cmsghdr *cmsgp = NULL;
char mbuf[CMSG_SPACE(sizeof *credp)];
/*
* Zero out mesage areas:
*/
memset(&msgh,0,sizeof msgh);
memset(mbuf,0,sizeof mbuf);
/*
* Establish datagram addres (if any):
*/
msgh.msg_name = addr;
msgh.msg_namelen = alen ? *alen : 0;
/*
* Point to our I/O vector of 1 element:
*/
msgh.msg_iov = iov;
msgh.msg_iovlen = 1;
/*
* initialize our 1 I/O element vector:
*/
iov[0].iov_base = buf;
iov[0].iov_len = bufsiz;
/*
* Initial control structure:
*/
msgh.msg_control = mbuf;
msgh.msg_controllen = sizeof mbuf;
/*
* Receive a messge:
*/
do
{
z = recvmsg(s,&msgh,0);
}while(z==-1 && errno==EINTR);
if(z==-1)
return -1; /* Failed: check errno */
/*
* If ptr alen is non-NULL,return the
* returned address length(datagrams):
*/
if(alen)
*alen = msgh.msg_namelen;
/*
* Walk the list of control messages:
*/
for(cmsgp = CMSG_FIRSTHDR(&msgh);
cmsgp != NULL;
cmsgp = CMSG_NXTHDR(&msgh,cmsgp))
{
if(cmsgp->cmsg_level == SOL_SOCKET
&& cmsgp->cmsg_type == SCM_CREDENTIALS)
{
/*
* Pass back credentials struct:
*/
*credp = *(struct ucred *)CMSG_DATA(cmsgp);
return z; /* # of data bytes read */
}
}
/*
* There were no credentials found.An error
* is returned here,since this application
* insists on getting credentials.
*/
errno = ENOENT;
return -1;
}
简单的Web服务器web80
这个非常简单的web服务器设计来操作两种方式:
不带有套接口服务器
带有套接口服务器
这将会有助于演示问题以及问题是如何解决的。下面列出了web80.c的模块。
/*
* web80.c:
*
* This is an extremely simple web server:
*
* This program runs in two modes:
*
* 1. Standalone Mode:
* $ ./web80 standalone
*
* In this mode,this program functions
* as a very simple standalone web server.
* However,it must run as root to bind
* to the web port 80.
*
* 2. Sock Server Mode:
* $ ./web80
*
* In this mode,this program contacts
* the sockserv server to request a
* socket bound to port 80. If sockserver
* allows the request,it returns
* a port 80 socket.This allows this
* program to run without root and
* with no setuid requirement.
*/
#include "common.h"
int main(int argc,char **argv)
{
int z;
int s; /* Web Server socket */
int c; /* Client socket */
int alen; /* Address length */
struct sockaddr_in a_web; /* Web server */
struct sockaddr_in a_clnt; /* Client server */
int b = TRUE; /* For SO_REUSEDADDR */
FILE *rx; /* Read Stream */
FILE *tx; /* Write Stream */
char getbuf[2048]; /* GET buffer */
time_t td; /* Current date & time */
/*
* If any arguments are present on the
* command line,obtain the socket
* without help from the server (run
* in simple standalone mode):
*/
if(argc>1)
{
/*
* Standalone mode:
*/
s = socket(PF_INET,SOCK_STREAM,0);
if(s==-1)
bail("socket(2)");
/*
* Web address on port 80:
*/
memset(&a_web,0,sizeof a_web);
a_web.sin_family = AF_INET;
a_web.sin_port = htons(80);
a_web.sin_addr.s_addr = htonl(INADDR_ANY);
/*
* Bind the Web server address-
* we need to be root to succeed
* at this:
*/
z = bind(s,(struct sockaddr *)&a_web,sizeof a_web);
if(z==-1)
bail("binding port 80");
/*
* Turn on SO_REUSEADDR:
*/
z = setsockopt(s,SOL_SOCKET,
SO_REUSEADDR,&b,sizeof b);
if(z==-1)
bail("setsockopt(2)");
}
else
{
/*
* Run in sockserv mode:
* Request a socket bound to port 80:
*/
s = reqport(80);
if(s==-1)
bail("reqport(80)");
}
/*
* Now make this a listening socket:
*/
z = listen(s,10);
if(z==-1)
bail("listen(2)");
/*
* Perform a simple,Web server loop for
* demonstration purposes.Here we just
* accept one line of input text,and
* ignore it.we provide one simple
* HTML page back in response:
*/
for(;;)
{
/*
* Wait for a connect from browser:
*/
alen = sizeof a_clnt;
c = accept(s,
(struct sockaddr *)&a_clnt,&alen);
if(c==-1)
{
perror("accept(2)");
continue;
}
/*
* Create streams for convenience,and
* just eat any web command provided:
*/
rx = fdopen(c,"r");
tx = fdopen(dup(c),"w");
fgets(getbuf,sizeof getbuf,rx);
/*
* Now server a simple HTML resonse.
* This includes this web server's
* process ID and the current date
* and time:
*/
fputs("<html>/n"
"<head>/n"
"<title>Test Page for this little "
"web80 server</title>/n"
"</head>/n"
"<body>/n"
"<h1>web80 worked!</h1>/n",tx);
time(&td);
fprintf(tx,
"<h2>From PID %ld @ %s</h2>/n",
(long)getpid(),
ctime(&td));
fputs("</body>/n"
"</html>/n",tx);
fclose(tx);
fclose(rx);
}
return 0;
}
reqport()函数
在我们上面所列的web80.c的程序中,我们可以看到有一个到reqport()的函数调用。这个函数负责与套接口服务器进行通信,并在80端口上得到一个套接口。函数代码如下:
/*
* reqport.c
*
* Request a port from the sockserv:
*/
#include "common.h"
/*
* Request a INADDR_ANY socket on the
* port number requested:
*
* Argument:
*
* s Socket to send request on
* port Port(host order) being requested
*
* Returns:
* >=0 Socket to use
* -1 Failed:check errno
*/
int reqport(int port)
{
int z;
int s; /* Socket */
struct sockaddr_un a_srvr; /* servr.adr */
/*
* Create a Unix Socket:
*/
s = socket(PF_LOCAL,SOCK_STREAM,0);
if(s==-1)
return -1; /* Failed: check errno */
/*
* Create the abstract address of
* the socket server:
*/
memset(&a_srvr,0,sizeof a_srvr);
a_srvr.sun_family = AF_LOCAL;
strncpy(a_srvr.sun_path,
"zSOCKET-SERVER",
sizeof a_srvr.sun_path-1);
a_srvr.sun_path[0]=0;
/*
* Connect to the sock server:
*/
z = connect(s,(struct sockaddr *)&a_srvr,sizeof a_srvr);
if(z==-1)
return -1; /* Failed:check errno */
/*
* Now issue our request:
*/
do
{
z = write(s,&port,sizeof port);
}while(z==1&&errno==EINTR);
if(z==-1)
return -1; /* Failed:see errno */
/*
* Now wait for a reply:
*/
z = recv_fd(s);
close(s);
return z; /* z==fd or -1 */
}
现在我们必须研究recv_fd()函数的工作过程,来了解文件描述符是如何接收的。
recv_fd()函数
下面给出了recv_fd()函数的代码:
/*
* recvfd.c
*
* Receive a file descriptor:
*/
#include "common.h"
/*
* Receive a file descriptor from the socket.
*
* Arguments:
*
* s Socket to receive file descriptor on
*
* Returns:
*
* >=0 Received file descriptor
* -1 Failed: see errno
*/
int recv_fd(int s)
{
int z;
struct msghdr msgh; /* Message header */
struct iovec iov[1]; /* I/O vector */
struct cmsghdr *cmsgp = NULL;
char buf[CMSG_SPACE(sizeof (int))];
char dbuf[80]; /* Small data buffer */
/*
* Initialize structures to zero bytes:
*/
memset(&msgh,0,sizeof msgh);
memset(buf,0,sizeof buf);
/*
* No socket address are used here:
*/
msgh.msg_name = NULL;
msgh.msg_namelen = 0;
/*
* Install our I/O vector:
*/
msgh.msg_iov = iov;
msgh.msg_iovlen = 1;
/*
* Initialize I/O vector to read data
* into our dbuf[] array:
*/
iov[0].iov_base = dbuf;
iov[0].iov_len = sizeof dbuf;
/*
* Load controll data into buf[]:
*/
msgh.msg_control = buf;
msgh.msg_controllen = sizeof buf;
/*
* Receive a message:
*/
do
{
z = recvmsg(s,&msgh,0);
}while(z==-1 && errno == EINTR);
if(z==-1)
return -1; /* Failed:see errno */
/*
* Walk the control structure looking for
* a file descriptor:
*/
for(cmsgp = CMSG_FIRSTHDR(&msgh);
cmsgp != NULL;
cmsgp = CMSG_NXTHDR(&msgh,cmsgp))
{
if(cmsgp->cmsg_level == SOL_SOCKET
&& cmsgp->cmsg_type == SCM_RIGHTS)
{
/*
* File descriptor found:
*/
return *(int *)CMSG_DATA(cmsgp);
}
}
/*
* No file descriptor was received:
* If we received 4 bytes,assume we
* recevied an errno value....then
* set errno from our received data.
*/
if(z == sizeof(int))
errno == *(int *)dbuf; /* Rcvf errno */
else
errno == ENOENT; /* Default errno */
return -1; /* Return failure indication */
}
在下面的内容中,我们将会看到套接口后面的代码。
sockserv服务器程序
套接口服务器是将会以root身份运行的服务器程序。这会赋予他在80端口上创建与绑定套接口的能力。然而,这是以这种权限运行的唯一组件。下面列出套接口服务器的代码:
/*
* sockserv.c:
*
* This simple server will serve up a socket
* to a valid recipient:
*/
#include "common.h"
#include <pwd.h>
/*
* Check user's access:
*
* Returns:
*
* ptr To (struct passwd *) if granted.
* NULL Access is to be denied.
*/
static struct passwd *
check_access(
int port, /* Port being requested */
struct ucred *pcred, /* User credentials */
char **uidlist) /* List of valid users */
{
int x;
struct passwd *pw; /* User passwd entry */
/*
* Look the user's uid # up in the
* /etc/password file:
*/
if((pw = getpwuid(pcred->uid)) != 0)
{
/*
* Make sure request is coming from
* one of the acceptable users:
*/
for(x=0;uidlist[x];++x)
if(!strcmp(uidlist[x],pw->pw_name))
break;
if(!uidlist[x])
pw = 0; /* Access denied */
}
/*
* Screen the port #. For this demo,
* only port 80 is permitted.
*/
if(port != 80)
pw = 0; /* Access denied */
return pw; /* NULL or prt if granted */
}
/*
* Access has been granted: send socket
* to client.
*
* Arguments:
* c Client socket
* port Port requested
*
* Returns:
* 0 Success
* -1 Failed: check errno
*/
static int grant_access(int c,int port)
{
int z;
int fd = -1; /* New socket fd */
int b = TRUE; /* Boolean TRUE */
struct sockaddr_in addr; /* word address */
/*
* Create a new TCP/IP socket:
*/
fd = socket(PF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket(2)");
goto errxit;
}
/*
* Tur on SO_REUSEADDR:
*/
z = setsockopt(fd,SOL_SOCKET,
SO_REUSEADDR,&b,sizeof b);
if(z==-1)
bail("setsockopt(2)");
/*
* Create the address to bind:
*/
memset(&addr,0,sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
/*
* Bind the requested address:
*/
z = bind(fd,(struct sockaddr *)&addr,sizeof addr);
if(z==-1)
{
fprintf(stderr,"%s: binding port %d/n",
strerror(errno),port);
goto errxit;
}
/*
* Send the fd back to the
* requesting client:
*/
z = send_fd(c,fd,NULL,0);
if(z==-1)
{
perror("send_fd()");
goto errxit;
}
close(fd); /* finished with fd */
return 0; /* Success */
errxit:
z = errno; /* Save errno */
if(fd)
close(fd); /* Release socket */
errno = z; /* Restore errno */
return -1;
}
/*
* Process a connected client's request:
*/
void process_client(
int c, /* Client socket */
char **uidlist /* List of valid users */
)
{
int z;
int er; /* Captured errno value */
int b; /* Boolean: Ture */
struct ucred cred; /* Clnt credentials */
short port; /* Port being requested */
struct paswd *pw; /* User passwd entry */
/*
* Now make certain that we can receive
* credentials on this socket:
*/
z = setsockopt(c,
SOL_SOCKET,
SO_PASSCRED,
&b,
sizeof b);
if(z==-1)
bail("setsockopt(2)");
/*
* Receive a request with the
* user credentials:
*/
z = recv_cred(c, /* Socket */
&cred, /* Return credentials */
&port, /* Returned port */
sizeof port, /* Size of data */
NULL,0); /* no socket address */
if(z==-1)
perror("recv_cred()");
/*
* Now check access.If pw is returned
* as non-NULL,the request is OK.
*/
pw = check_access(port,&cred,uidlist);
if(pw)
{
if(!grant_access(c,port))
{
close(c);
return; /* request successful */
}
/* Failed */
er = errno; /* Capture reason */
}
else
{
/*
* Userid was not know,or not in
* the privileged list:
*/
er = EACCES; /* perm denied */
}
/*
* Control reaches here if the
* request failed or is denied:
*
* Here we simply send the error
* code back without a file
* descriptor.This lack of a fd
* will be detected by the client.
*/
do
{
z = write(c,&er,sizeof er);
}while(z==-1&&errno==EINTR);
if(z==-1)
perror("write(2)");
}
/*
* Main program:
*/
int main(int argc,char **argv)
{
int z;
int s; /* Server UDP socket */
int c; /* Client socket */
int alen; /* Address length */
struct sockaddr_un a_srvr; /* Server @ */
struct sockaddr_un a_clnt; /* Client @ */
/*
* Make sure we have a userid specified:
*/
if(argc<2)
{
fputs("Must have at least 1 userid./n",
stderr);
exit(1);
}
/*
* Create a Unix Socket:
*/
s = socket(PF_LOCAL,SOCK_STREAM,0);
if(s==-1)
bail("socket(2)");
/*
* Create abstract address:
*/
memset(&a_srvr,0,sizeof a_srvr);
a_srvr.sun_family = AF_LOCAL;
strncpy(a_srvr.sun_path,
"zSOCKET-SERVER",
sizeof a_srvr.sun_path-1);
a_srvr.sun_path[0]=0;
/*
* Bind the server address:
*/
z = bind(s,
(struct sockaddr *)&a_srvr,
sizeof a_srvr);
if(z==-1)
bail("bind(2)");
/*
* Now make this listening socket:
*/
z = listen(s,10);
if(z==-1)
bail("listen(2)");
/*
* Now process requests:
*/
for(;;)
{
/*
* Wait for a connect:
*/
alen = sizeof a_clnt;
c = accept(s,(struct sockaddr *)&a_clnt,&alen);
if(c==-1)
bail("accept(2)");
/*
* Process this request:
*/
process_client(c,argv+1);
close(c);
}
return 0;
}
send_fd()函数
套接口服务器调用send_fd()函数来将创建并绑定的套接口发送回请求进程。下面给出这个函数的代码:
/*
* sendfd.c
*
* Send a file descriptor:
*/
#include "common.h"
/*
* Send a file descriptor via socket:
*
* Arguments:
*
* s Socket to send on
* fd Open file descriptor to send
* addr Ptr to udp address or NULL
* alen Size of addr or zero
*
* Returns:
*
* 0 Successful
* -1 Failed: check errno
*/
int send_fd(int s,int fd,void *addr,socklen_t alen)
{
int z;
struct msghdr msgh; /* Message header */
struct iovec iov[1]; /* I/O vector */
struct cmsghdr *cmsgp = NULL;
char buf[CMSG_SPACE(sizeof fd)];
int er=0; /* No error code of zero */
/*
* Clear message areas:
*/
memset(&msgh,0,sizeof msgh);
memset(buf,0,sizeof buf);
/*
* Supply socket address (if any):
*/
msgh.msg_name = addr;
msgh.msg_namelen = alen;
/*
* Install our I/O vector:
*/
msgh.msg_iov = iov;
msgh.msg_iovlen = 1;
/*
* Initialize the I/O vector to send
* the value in er(which is zero).
* This is done because data must be
* transmitted to send the fd.
*/
iov[0].iov_base = &er;
iov[0].iov_len = sizeof er;
/*
* Establish control buffer:
*/
msgh.msg_control = buf;
msgh.msg_controllen = sizeof buf;
/*
* Configure the message to send
* a file descriptor:
*/
cmsgp = CMSG_FIRSTHDR(&msgh);
cmsgp->cmsg_level = SOL_SOCKET;
cmsgp->cmsg_type = SCM_RIGHTS;
cmsgp->cmsg_len = CMSG_LEN(sizeof fd);
/*
* Install the file descriptor value:
*/
*((int *)CMSG_DATA(cmsgp)) = fd;
msgh.msg_controllen = cmsgp->cmsg_len;
/*
* Send it to the client process:
*/
do
{
z = sendmsg(s,&msgh,0);
}while(z==-1 && errno == EINTR);
return z==-1 ? -1 : 0;
}
现在我们已经看到了这个例子中所用到的所有源代码了。下面,我们要来测试这些代码。
测试套接口服务器
首先,我们要编译这些模块:
$ make
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type web80.c
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type misc.c
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type reqport.c
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type recvfd.c
gcc web80.o misc.o reqport.o recvfd.o -o web80
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type sockserv.c
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type recvcred.c
gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type sendfd.c
gcc sockserv.o misc.o recvcred.o sendfd.o -o sockserv
$
make过程最终将会生成两个可执行文件:
套接口服务器程序sockserv
简单的Web服务器web80
现在,我们以非root权限来测试我们简单的Web服务器(一定要提供一个命令行参数):
$ ./web80 stand_alone
Permission denied: binding port 80
$
任何的命令行参数,例如上面所示的stand_alone,将会使得web80程序试着了80端口创建其套接口。正如我们所看到的,如果没有root权限,内核就会禁止这种行为。
测试sockserv
现在,我们可以在一个终端会话或是窗口中以root身份启动套接口服务器。提供一个或是多个用户ID作为命令行参数,这时内核会允许请求得到一个80端口的套接口。如下的例子所示:
$ su root
Password:
# ./sockserv fred &
[1] 1077
#
现在我们已经使得我们的套接口服务器运行了,启动另一个终端会话或是窗口来运行我们的Web服务器。但是我们要记得要使用合适的用户登陆(在这个例子中我们使用用户ID fred):
$ ./web80 &
[1] 1079
$
在这个例子中,进程ID为1079。我们要在心里记住这一点。web80启动而没有报告任何错误,这表明他已经成功的从套接口服务器得到了一个套接口。要证明这一点,我们可以启动Web浏览器,使用网址http://127.0.0.1来进行测试。如果我们由网络上的其他主机进行连接,我们需要提供正确的主机名或是IP地址。我们浏览的报告类似如下的内容:
web80 Worked!
From PID 1079 @ Sat Nov 20 12:26:00 1999
上面的输出显示了我们的浏览器所接收到的简单的HTML响应。注意,所报告的进程ID与我们的web80进程相匹配。
另一个高效且简单的方法是用下面的方法来测试web80服务器:
$ telnet 127.0.0.1 80
Trying 127.0.0.1 . . .
Connected to 127.0.0.1.
Escape character is '^]'.
GET /something
<HTML>
<HEAD>
<TITLE>Test Page for this little web80 server</TITLE>
</HEAD>
<BODY>
<H1>web80 Worked!</H1>
<H2>From PID 1079 @ Sat Nov 20 12:39:26 1999
</H2>
</BODY>
</HTML>
Connection closed by foreign host.
$
使用telnet过程,我们需要提供一行输入(GET /something)。