linux+httpd+svn实现http访问及修改口令

1.背景

本人团队原来使用VisualSVN工具搭建svn服务器,随着时间的增长,windows的磁盘空间越来越大,最后满了,也不太好扩容(当然如果想扩容,最终肯定是可以实现的)。为了以后更好的svn容量管理,就开始新搭建一台linux服务器,将svn数据单独放存储上,这样后期扩容就很方便了。
简单一句就是:从VisualSVN迁移数据至Linux版svn中。

2.原有功能

  • 协议为http://方式(web方式或客户端方式均支持)
  • 可在线修改用户自己口令
  • 库名都是中文的

3.安装过程

3.1.软件版本

为支持原有功能,本人先在网上搜索了很多,然后参考linux下apache+SVN搭建完美版进行搭建。
当然本次迁移,本人使用了最新的软件版本,清单如下:

  • Apr: apr-1.5.2和apr-util-1.5.4
  • Apache: httpd-2.4.25
  • Subversion: subversion-1.9.5
  • Zlib: zlib-1.2.11
  • Sqlite: sqlite-autoconf-3170000
  • Pcre: pcre-8.40
    各个软件包可以在官网下载,如何解压见参考文。

3.2.软件编译参数

上述各个软件编译时的参数清单如下(有软件有安装顺序):

  • ./configure --prefix=/svn/bin/apr/
  • ./configure --prefix=/svn/bin/apr-util/ --with-apr=/svn/bin/apr/
  • ./configure --prefix=/svn/bin/sqlite/
  • ./configure --prefix=/svn/bin/zlib/
  • ./configure --prefix=/svn/bin/pcre/
  • ./configure --prefix=/svn/bin/httpd/ --enable-so --enable-dav --enable-dav-fs --enable-maintainer-mode --enable-deflate=shared --enable-expires=shared --enable-headers=shared --enable-rewrite=shared --enable-static-support --with-mpm=prefork --enable-cache --enable-file-cache --enable-rewrite --enable-ssl --enable-proxy --enable-proxy-http --with-apr=/svn/bin/apr/ --with-apr-util=/svn/bin/apr-util/ --with-sqlite=/svn/bin/sqlite/ --with-pcre=/svn/bin/pcre/
  • ./configure --prefix=/svn/bin/svn --with-apr=/svn/bin/apr --with-apr-util=/svn/bin/apr-util/ --with-sqlite=/svn/bin/sqlite/ --with-zlib=/svn/bin/zlib/ --with-apxs=/svn/bin/httpd/bin/apxs --with-apache-libexecdir=/svn/bin/httpd/modules --enable-mod-activation
    具体各个参数含义,可通过./configure --help显示,或者查找官方说明。
    安装及后续配置时建议均使用同一个用户,如本文的svnapp用户,就会少很多目录权限的问题。

3.3.安装完结果

按照上述安装完后,如果没有异常发生,那么应该存在以下重要信息:

  • /svn/bin/httpd/modules/目录下应有mod_dav_svn.so和mod_zuthz_svn.so两个文件
  • /svn/bin/httpd/conf/httpd.conf文件中已经自动LoadModule上面2个*.so文件

4.配置过程

4.1.svn建库

mkdir -p /svndata/Repositories/测试;
/svn/bin/svn/bin/svnadmin create --pre-1.6-compatible /svndata/Repositories/测试

为了与原有客户端兼容(原来VisualSVN为1.7版本),需添加--pre-1.6-xx参数或者--pre-1.7-xx参数
此时,【/svndata/Repositories/测试】目录下应自动生成一些配置文件。
验证svn是否可用:
将/svndata/Repositories/测试/conf/svnserve.conf中anon-access参数修改为write
然后启动svn【/svn/bin/svn/bin/svnserve -d -r /svndata/Repositories/】,使用svn客户端,如TortoiseSVN访问【svn://IP:9000/测试】应该可以正常访问,且可以新增文件或文件夹,表示svn使用正常

4.2.svn角色权限配置

参考文linux下apache+SVN搭建完美版中已经完美了。
本文中,用户及权限文件存放于【/svndata/Repositories/svn_conf/】目录下,文件名为authz及passwd(密码为明文,因为svn只支持明文)
然后修改【/svndata/Repositories/测试/conf/svnserve.conf】中对应的密码及权限文件,可以使用绝对路径,这样权限角色及密码就统一管理了。
如果只是使用一个库,那么就省去单独配置,直接使用conf下的配置文件进行配置即可。

4.3.apache配置

/svn/bin/httpd/conf/httpd.conf:
1、Listen 80修改为9000端口(如果使用80端口,则需要root用户启动) 2、User daemon及Group daemon分别修改为svnapp和svngrp,即svn建库用户与apaache启动用户为同一个,就不用chmod命令了 3、ServerName 修改为自己的IP地址 4、文件最后添加一行Include conf/httpd-svn.conf,把所有svn的配置都放在/svn/bin/httpd/conf/httpd-svn.conf文件中,方便管理
然后在conf目录中touch httpd-svn.conf
此时可以使用命令启动svn及httpd服务:

  • /svn/bin/httpd/bin/apachectl -k start
    可以使用ps -fu svnapp查看这些进程。
    至此,apache和svn是独立启动访问的,保证都ok后,再往下走。

4.4.关联apache和svn

/svn/bin/httpd/conf/httpd-svn.conf:

SVNUseUTF8 On
<Location /svn>
  DAV svn
  SVNParentPath /svndata/Repositories
  SVNListParentPath On
  SVNIndexXSLT "/svnindex.xsl"
  SVNPathAuthz short_circuit
  AuthType Basic
  AuthName "Subversion Repository"
  AuthUserFile /svndata/Repositories/svn_conf/htpasswd
  AuthzSVNAccessFile /svndata/Repositories/svn_conf/authz
  Require valid-user
</Location>
CustomLog logs/svn_logfile "%t %u %{SVN-ACTION}e" env=SVN-ACTION

大部分指令在参考网页中已经说明,现对特殊的简要说明:

  • SVNUseUTF8指令是svn 1.8版本以后的新功能,告诉mod_dav_svn用utf8编码解析URL及文件名。此处若不添加,则可能会报【Can't convert string from 'UTF-8' to natvie encoding】的错,详情见why repository name should not be utf-8,此问题本人寻找了好几天,终于在某个网页上提及了这个指令,然后报着试一试的想法,果然能解决。
  • SVNIndexXSLT指令表示web端访问时,要采用的样式,这是一个xml的文档定义表,即定义目录名、文件名、上一层等显示的css样式,本文使用的代码见后文。
  • CustomLog指令为svn正常访问的日志重定向,可用可不用

然后使用htpasswd命令创建1个用户,然后设置好权限authz文件(这些均见参考网页)。
至此,应该可以使用“ http://IP:9000/svn/测试 ”在浏览器中访问了(可先把SVNIndexXSLT指令注释掉,再测试)

4.5.Web显示样式

这几个显示的样式文件及js文件,均从VisualSVN中复制过来,然后稍做修改而得
/svn/bin/httpd/htdocs/svnindex.xsl(从VisualSVN来,未修改):

<?xml version="1.0"?>
<!-- A sample XML transformation style sheet for displaying the Subversion
  directory listing that is generated by mod_dav_svn when the "SVNIndexXSLT"
  directive is used. -->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="html"/>
  <xsl:template match="*"/>
  <xsl:template match="svn">
    <html>
      <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <title>
          <xsl:if test="index/@base">
            <xsl:text>代码库 </xsl:text>
            <xsl:value-of select="index/@base" /> 
            <xsl:text>:</xsl:text>
            <xsl:value-of select="index/@path"/>
          </xsl:if>
          <xsl:if test="string-length(index/@base) = 0">
            <xsl:text>仓库列表</xsl:text>
          </xsl:if>
        </title>
        <link rel="stylesheet" type="text/css" href="/svnindex.css"/>
        <script language="javascript" type="text/javascript" src="/jquery.js" charset="utf-8"></script>
        <script language="javascript" type="text/javascript" src="/svnAuth.js" charset="utf-8"></script>
      </head>
      <body>
        <div class="header" id="header">
          <xsl:element name="a">
            <xsl:attribute name="id">
              <xsl:text>lnkModify</xsl:text>
            </xsl:attribute>
            <xsl:attribute name="style">
              <xsl:text>color:green;text-decoration:none;</xsl:text>
            </xsl:attribute>
            <xsl:attribute name="href">
              <xsl:text>javascript:doChange();void(0);</xsl:text>
            </xsl:attribute>
            <xsl:text>修改密码</xsl:text>
          </xsl:element>
          <xsl:element name="span">
            <xsl:text> </xsl:text>
          </xsl:element>
          <xsl:element name="span">
            <xsl:text> </xsl:text>
          </xsl:element>
          <xsl:element name="a">
            <xsl:attribute name="style">
              <xsl:text>color:green;text-decoration:none;</xsl:text>
            </xsl:attribute>
            <xsl:attribute name="href">
              <xsl:text>http://www.visualsvn.com/doc/</xsl:text>
            </xsl:attribute>
            <xsl:text>帮助文档</xsl:text>
          </xsl:element>
        </div>
        <div class="svn">
          <xsl:apply-templates/>
        </div>
        <div class="footer">
          <xsl:element name="a">
            <xsl:attribute name="href">
              <xsl:text>http://www.visualsvn.com/server/</xsl:text>
            </xsl:attribute>
            <xsl:text>VisualSVN Server</xsl:text>
          </xsl:element>

          <xsl:text>&#160;&#160;  powered by </xsl:text>
          <xsl:element name="a">
            <xsl:attribute name="href">
              <xsl:value-of select="@href"/>
            </xsl:attribute>
            <xsl:attribute name="title">
              <xsl:text>ver </xsl:text>
              <xsl:value-of select="@version"/>
            </xsl:attribute>
            <xsl:text>Subversion </xsl:text>
          </xsl:element>
        </div>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="index">
    <div class="rev">
      <xsl:value-of select="@name"/>
      <xsl:if test="@base">
        <xsl:text>代码库 </xsl:text>
        <xsl:value-of select="@base" />
        <xsl:text>:</xsl:text>
        <xsl:value-of select="@path"/>
        <xsl:text>&#160;&#160;</xsl:text>
        <xsl:if test="@rev">
          <xsl:text>当前版本 Rev.</xsl:text>
          <xsl:value-of select="@rev"/>
        </xsl:if>
      </xsl:if>
      <xsl:if test="string-length(@base) = 0">
        <xsl:text>仓库列表</xsl:text>
      </xsl:if>
    </div>
    <xsl:if test="@base">
      <div class="dir">
        <xsl:element name="a">
          <xsl:attribute name="href">/svn/</xsl:attribute>
          ![](/dir.png)
          <xsl:text>返回仓库列表</xsl:text>
        </xsl:element>
      </div>
    </xsl:if>
    <xsl:apply-templates select="updir"/>
    <xsl:apply-templates select="dir"/>
    <xsl:apply-templates select="file"/>
  </xsl:template>

  <xsl:template match="updir">
    <div class="dir">
      <xsl:element name="a">
        <xsl:attribute name="href">..</xsl:attribute>

        ![](/dir.png)
        <xsl:text>&#160;</xsl:text>
        <xsl:text>..</xsl:text>

      </xsl:element>
    </div>
  </xsl:template>

  <xsl:template match="dir">
    <div class="dir">
      <xsl:element name="a">
        <xsl:attribute name="href">
          <xsl:value-of select="@href"/>
        </xsl:attribute>

        ![](/dir.png)
        <xsl:text>&#160;</xsl:text>
        <xsl:value-of select="@name"/>

        <xsl:text>/</xsl:text>
      </xsl:element>
    </div>
  </xsl:template>

  <xsl:template match="file">
    <div class="file">
      <xsl:element name="a">
        <xsl:attribute name="href">
          <xsl:value-of select="@href"/>
        </xsl:attribute>

        ![](/file.png)
        <xsl:text>&#160;</xsl:text>
        <xsl:value-of select="@name"/>

      </xsl:element>
    </div>
  </xsl:template>

</xsl:stylesheet>

该文件引用的文件有/svn/bin/httpd/htdocs/jquery.js,直接从官网下载压缩版的即可。
文件/svn/bin/httpd/htdocs/svnindex.css(从VisualSVN来,未修改):

/* Style sheet for displaying the Subversion directory listing
   that is generated by mod_dav_svn and "svnindex.xsl". */

body{
  font-family:Verdana, Arial, Helvetica, sans-serif;
  font-size:9pt;
  background: #ffffff;
  margin: 0;
  padding: 0;
}

img { border: none; }

a {
  color: navy;
}

.header
{
  text-align: right;
  margin-right: 0.5em;
  clear: both;
  margin-left: 0.5em;
  background: url(header.png) top left no-repeat;
  height: 48px;
  padding: 6px;
}

.footer {
  text-align: right;
  margin-top: 1em;
  padding: 0.5em 1em 0.5em;
  font-size: 80%;
}

.svn {
  margin-left: 0.5em;
  margin-right: 0.5em;
}

.rev {
  margin-right: 3px;
  padding-left: 3px;
  text-align: left;
  font-size: 120%;
}

.dir a {
  text-decoration: none;
  color: black;
  display: block;
}

.dir img { vertical-align: middle }

.file a {
  text-decoration: none;
  color: black;
  display: block;
}

.file {
  margin: 3px;
  padding: 3px;
  background: rgb(95%,95%,95%);
}

.file img { vertical-align: middle }

.file:hover {
  margin: 3px;
  padding: 3px;
  background: rgb(90%,100%,90%);
}

.dir {
  margin: 3px;
  padding: 3px;
  background: rgb(90%,90%,90%);
}

.dir:hover {
  margin: 3px;
  padding: 3px;
  background: rgb(80%,100%,80%);
}

还有文件/svn/bin/httpd/htdocs/svnAuth.js(从VisualSVN来,增加txt = txt.replace(/[\n]/ig, '')语句,增加"&type=CHG"和"&type=NEW"两条语句):

var svnpassURL="/cgi-bin/svnpass.py";
String.prototype.encodeURI=function() {var rS; rS=escape(this); return rS.replace(/\+/g,"%2B");};
function doChange() 
{ 
    if (document.getElementById("creatInfo")) $("#creatInfo").remove();
    if (!document.getElementById("pwInfo"))
    {
        $('<div id="pwInfo" style="background-color:#f3f3f3;padding:0px;"> '
        +'用户名:<input type="text" id="username" size="10" /> '
        +'密 码:<input type="password" id="oldpwd" size="8" /> '
        +'新密码:<input type="password" id="newpwd" size="8" /> '
        +'新密码确认:<input type="password" id="newpwdCfm" size="8" /> '
        +'<input type="button" id="btnSubmit" value="确认修改" /> '
        +'<input type="button" id="btnCancel" value="取消修改" /> '
        +'</div>').insertBefore("#header");
        $("#btnSubmit").bind("click", execChange);
        $("#btnCancel").bind("click", function() { $("#pwInfo").remove(); } );
    }
}

function execChange()
{
    var u=function(txt) 
    { 
                txt = txt.replace(/[\n]/ig, '')
        if (txt=="0")
        {
            alert("操作成功!");
            $("#pwInfo").remove();
        }
        else
        {
            alert(txt);
            $("#btnSubmit").attr("value", "确认修改").removeAttr("disabled");
        }
    };
    $("#btnSubmit").attr("value", "请稍等...").attr("disabled","disabled");
    jQuery.post(svnpassURL,
        "username="+$("#username").val().encodeURI()
        +"&oldpwd="+$("#oldpwd").val().encodeURI()
        +"&newpwd="+$("#newpwd").val().encodeURI()
        +"&newpwdcfm="+$("#newpwdCfm").val().encodeURI()
                +"&type=CHG",
    u)
}

function doCreate() 
{ 
    if (document.getElementById("pwInfo")) $("#pwInfo").remove();
    if (!document.getElementById("creatInfo"))
    {
        $('<div id="creatInfo" style="background-color:#D1EDD1;padding:0px;"> '
        +'用户名:<input type="text" id="usernameApply" size="10" /> '
        +'登录密码:<input type="password" id="pwd" size="8" /> '
        +'密码确认:<input type="password" id="pwdCfm" size="8" /> '
        +'<input type="button" id="btnApply" value="确认申请" /> '
        +'<input type="button" id="btnApplyCancel" value="取消申请" /> '
        +'</div>').insertBefore("#header");
        $("#btnApply").bind("click", execCreat);
        $("#btnApplyCancel").bind("click", function() { $("#creatInfo").remove(); } );
    }
}

function execCreat()
{
    var u=function(txt) 
    { 
                txt = txt.replace(/[\n]/ig, '')
        if (txt=="0")
        {
            alert("操作成功!");
            $("#creatInfo").remove();
        }
        else
        {
            alert(txt);
            $("#btnApply").attr("value", "确认申请").removeAttr("disabled");
        }
    };
    $("#btnApply").attr("value", "请稍等...").attr("disabled","disabled");
    jQuery.post(svnpassURL,
        "username="+$("#usernameApply").val().encodeURI()
        +"&pwd="+$("#pwd").val().encodeURI()
        +"&pwdcfm="+$("#pwdCfm").val().encodeURI(),
                +"&type=NEW",
    u)
}


$(document).bind('ready',function(){
    if (document.location.href.toLowerCase().indexOf("/svnauth/")!=-1)
    {
        $("#lnkModify").before('<a href="javascript:doCreate();void(0);" style="color:green;text-decoration:none;">创建新用户</a>&nbsp;&nbsp;'); 
    }
});

当然,除了这3个文件外,还有图片,请读者自行添加即可。

至此,web浏览器访问出来的效果就很好看了。但还无法进行web端修改密码。

5.实现客户端修改口令

本人在网上找了很多,大部分是PHP的脚本,还需要安装PHP,感觉并不太方便,也没有解释原理,本人实现效果如下:


133965876475086475.jpg

现整理下实现原理如下:

  • WEB端弹出的密码框由上面的svnAuth.js动态控制
  • 真正实现修改口令的是cgi脚本,本文直接使用python提供

因此,本人就偷懒了下,直接把VisualSVN的前端页面(如上述的css和js文件)拿过来了,这样就不用重复造轮子了。
但由于VisualSVN执行的cgi程序svnpass为二进制文件,无法直接放到linux上执行,所以就放弃了。
这里得说下,网上大多使用的是apache2xPasswd和apache2xPasswd.ini文件作为cgi的后台支持。但本人找了下,好像还没有apache24Passwd的文件,而且这还是二进制的文件,太方便修改了。所以本人还是自己动手吧,最终发现其实并不复杂。
现直接亮出python脚本:/svn/bin/httpd/cgi-bin/svnpass.py:

#!/usr/bin/python
#-*- coding: UTF-8

import os
import cgi
import subprocess

pwdfile = "/svndata/Repositories/svn_conf/htpasswd"
cmdfile = "/svn/bin/httpd/bin/htpasswd"

errmsg = ""

def run_script(c):
    try :
        res = subprocess.check_output(c, shell=True)
        return True
    except Exception as exp:
        errmsg = exp.output
        return False

def check(n, p):
    cmd = "%s -bv %s %s %s" % (cmdfile, pwdfile, n, p)
    #print "check: ", cmd
    return run_script(cmd)

def setPwd(n, p):
    cmd = "%s -D %s %s;%s -b %s %s %s" % (cmdfile, pwdfile, n, cmdfile, pwdfile, n, p)
    return run_script(cmd)

def main():
    print "Content-type: text/html; charset=utf-8\n"

    form = cgi.FieldStorage()

    name    = form.getvalue("username" , "").strip()
    optype  = form.getvalue("type"     , "").strip()
    ## create
    pwd     = form.getvalue("pwd"      , "").strip()
    pwdcfm  = form.getvalue("pwdcfm"   , "").strip()
    ## change
    oldpwd  = form.getvalue("oldpwd"   , "").strip()
    newpwd1 = form.getvalue("newpwd"   , "").strip()
    newpwd2 = form.getvalue("newpwdcfm", "").strip()

    if name == "" or optype == "" :
        print "缺少参数用户名或者类型"
        return

    ## create
    if optype == "NEW" :
        if pwd == "" or pwdcfm == "" or pwd != pwdcfm :
            print "密码不能为空或两次密码输入不一致!"
        else :
            print ("0" if setPwd(name, pwd) else "设置密码失败:" + errmsg)
    elif optype == "CHG" :
        if oldpwd == "" or newpwd1 == "" or newpwd2 == "" or newpwd1 != newpwd2 :
            print "密码不能为空或两次密码输入不一致!"
        else :
            if not check(name, oldpwd) :
                print "原始密码验证失败"
            else :
                print ("0" if setPwd(name, newpwd1) else "修改密码失败:" + errmsg)
    else :
        print "类型不正确!"

    ## for key in os.environ.keys():
    ##     print "%s = %s<br/>" % (key, os.environ[key])

main()

此脚本就是当前端svnAuth.js执行jquery.post()方法时,异步的返回是否修改口令成功,如果返回"0",表示成功,返回其它字符表示出错了。当然这种前后台接口是最low的,但对于svn这种工具,也没有必要花心思去设计那么完美的接口,比如什么json、restfull之类的东东。
总结起来核心点是:

  • 使用/svn/bin/httpd/bin/htpasswd命令行工具进行验证、修改用户口令

那么也就是说大家熟悉哪门语言或者说linux服务器上支持哪门语言,就可以直接写这样类似的脚本。本人当前机器刚好已经自带了python2.7,所以就直接用python去写了。
然后设置一下svnpass.py的执行权限为+x。

至此,所有的配置就已经完成了。

经过这快2周的调试,总算弄明白这svn通过http方式访问的原理,而且用户能自己修改口令。其实我真希望svn官网能直接提供这种支持,让用户使用起来更加方便。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容