在多个登录会话中共享相同的“ ssh-agent”


61

有没有一种简便的方法来确保来自给定用户(即我)的所有登录都使用相同的ssh-agent?我破解了一个脚本,使它大部分时间都可以正常工作,但是我一直怀疑我错过了某种方法。此外,自那时以来,计算机技术有了惊人的进步,例如本网站。

所以这里的目标是

  • 每当我登录到该框时,无论它是通过SSH还是在从gdm / kdm / etc启动的图形会话中,还是在控制台上:
    • 如果我的用户名当前没有ssh-agent运行,则启动,导出并ssh-add调用环境变量。
    • 否则,现有座席的坐标将导出到登录会话的环境变量中。

当所涉及的盒子在ssh进入第三个盒子时用作中继点时,此功能特别有价值。在这种情况下,它避免了每次ssh进入然后想要执行git push某事时都必须键入私钥的密码。

下面给出的脚本可以可靠地完成此任务,尽管它最近在X崩溃时崩溃了,然后我开始了另一个图形化会话。在这种情况下,可能还有其他问题。

这是我的好坏脚本。我从我的来源.bashrc

# ssh-agent-procure.bash
# v0.6.4
# ensures that all shells sourcing this file in profile/rc scripts use the same ssh-agent.
# copyright me, now; licensed under the DWTFYWT license.

mkdir -p "$HOME/etc/ssh";

function ssh-procure-launch-agent {
    eval `ssh-agent -s -a ~/etc/ssh/ssh-agent-socket`;
    ssh-add;
}

if [ ! $SSH_AGENT_PID ]; then
  if [ -e ~/etc/ssh/ssh-agent-socket ] ; then
    SSH_AGENT_PID=`ps -fC ssh-agent |grep 'etc/ssh/ssh-agent-socket' |sed -r 's/^\S+\s+(\S+).*$/\1/'`; 
    if [[ $SSH_AGENT_PID =~ [0-9]+ ]]; then
      # in this case the agent has already been launched and we are just attaching to it. 
      ##++  It should check that this pid is actually active & belongs to an ssh instance
      export SSH_AGENT_PID;
      SSH_AUTH_SOCK=~/etc/ssh/ssh-agent-socket; export SSH_AUTH_SOCK;
    else
      # in this case there is no agent running, so the socket file is left over from a graceless agent termination.
      rm ~/etc/ssh/ssh-agent-socket;
      ssh-procure-launch-agent;
    fi;
  else
    ssh-procure-launch-agent;
  fi;
fi;

请告诉我,有一种更好的方法可以做到这一点。另外,请不要挑剔不一致/失误(例如,放入var东西etc);我前一阵子写了这篇,从那以后学到了很多东西。


1
KeyError:找不到'DWTFYWT';您是说WTFPLv2吗?
grawity 2010年

@grawity:感谢您提供的链接,他们的常见问题解答使我感动: 顺便说一句,通过WTFPL,我也可以…… 哦,是的,当然可以。 但是我可以... 是的,可以。 可以... 是的! hahahahahaha
quack quixote 2010年

@grawity:不,那正是我想让您想到的,mwahahaha。
直觉

Answers:


25

我不妨将自己的变体形式添加到组合中:

function sshagent_findsockets {
    find /tmp -uid $(id -u) -type s -name agent.\* 2>/dev/null
}

function sshagent_testsocket {
    if [ ! -x "$(which ssh-add)" ] ; then
        echo "ssh-add is not available; agent testing aborted"
        return 1
    fi

    if [ X"$1" != X ] ; then
        export SSH_AUTH_SOCK=$1
    fi

    if [ X"$SSH_AUTH_SOCK" = X ] ; then
        return 2
    fi

    if [ -S $SSH_AUTH_SOCK ] ; then
        ssh-add -l > /dev/null
        if [ $? = 2 ] ; then
            echo "Socket $SSH_AUTH_SOCK is dead!  Deleting!"
            rm -f $SSH_AUTH_SOCK
            return 4
        else
            echo "Found ssh-agent $SSH_AUTH_SOCK"
            return 0
        fi
    else
        echo "$SSH_AUTH_SOCK is not a socket!"
        return 3
    fi
}

function sshagent_init {
    # ssh agent sockets can be attached to a ssh daemon process or an
    # ssh-agent process.

    AGENTFOUND=0

    # Attempt to find and use the ssh-agent in the current environment
    if sshagent_testsocket ; then AGENTFOUND=1 ; fi

    # If there is no agent in the environment, search /tmp for
    # possible agents to reuse before starting a fresh ssh-agent
    # process.
    if [ $AGENTFOUND = 0 ] ; then
        for agentsocket in $(sshagent_findsockets) ; do
            if [ $AGENTFOUND != 0 ] ; then break ; fi
            if sshagent_testsocket $agentsocket ; then AGENTFOUND=1 ; fi
        done
    fi

    # If at this point we still haven't located an agent, it's time to
    # start a new one
    if [ $AGENTFOUND = 0 ] ; then
        eval `ssh-agent`
    fi

    # Clean up
    unset AGENTFOUND
    unset agentsocket

    # Finally, show what keys are currently in the agent
    ssh-add -l
}

alias sagent="sshagent_init"

然后,每次登录时,如果我附加一个代理(并非总是如此),只需键入即可sagent


2
if [ ! -x "$(which ssh-add)" ];应该用if ! which ssh-add;或代替if ! command -v ssh-add。(请记住,[这只是命令)
10年

好的,您可以执行此操作,但是实际上需要if ! which ssh-add > /dev/null防止打印该路径,在这一点上,我不确定它是否更清晰,尽管我认为它确实为您节省了一次额外的命令调用。
Zed 2010年

所以基本上答案是否定的。废话 好吧,这看起来比我的hack演变得更多,因此可能会很有用。奇怪的是,虽然没有一种更结构化的方式来执行此操作,但似乎很有用。
直觉

这些天,我很适合重新存储所有内容,因此我为您的脚本设置了一个github存储库。再次感谢。我希望我已经足够正式的许可了:〜/
亲切感

我不介意 不过,如果有人对此进行了改进,请在此处发表评论。
Zed 2010年

36

ssh -A [user@]remotehost

我认为这可能是您要寻找的。运行ssh时,请使用-A开关来转发ssh-agent。这是一个用例:

我有一台远程服务器,上面有一些git repos,并有一个指向github的远程指针。如果在屏幕会话中没有运行ssh-agent,则我必须输入密钥的密码才能执行“ git pull origin master”。!另外,我必须在远程服务器上安装我的私钥 -更多Boooo!

相反,只需ssh -A [user@]remotehost沿本地运行的ssh-agent 使用传递即可。现在,我不再需要我的私钥甚至可以存在于远程主机上。我认为您根本不需要使用ssh-agent编写任何脚本。


4
对此一无所知,但事实证明,这正是我解决这个问题时一直在寻找的东西。
Will McCutchen

1
这甚至比我要找的要好!好答案!
WhyNotHugo 2013年

1
又见man 5 ssh_configForwardAgent配置设置。默认情况下,它启用代理转发,而无需使用-A参数。使用代理转发之前,请注意存在安全风险,远程计算机上的其他特权用户可能会访问转发的代理套接字。手册页中也提到了这一点。这在这里很好地解释
starfry 2014年

我认为必须在服务器上将AllowAgentForwarding选项设置为yes
Ziofil

20

这是一个在Cygwin中也可以使用的不错的工具:

SSH_ENV=$HOME/.ssh/environment

function start_agent {
     echo "Initialising new SSH agent..."
     /usr/bin/ssh-agent | sed 's/^echo/#echo/' > ${SSH_ENV}
     echo succeeded
     chmod 600 ${SSH_ENV}
     . ${SSH_ENV} > /dev/null
     /usr/bin/ssh-add;
}

# Source SSH settings, if applicable

if [ -f "${SSH_ENV}" ]; then
     . ${SSH_ENV} > /dev/null
     #ps ${SSH_AGENT_PID} doesn't work under cywgin
     ps -efp ${SSH_AGENT_PID} | grep ssh-agent$ > /dev/null || {
         start_agent;
     }
else
     start_agent;
fi

将其添加到您的.bash_profile或.bashrc

来源:http//www.cygwin.com/ml/cygwin/2001-06/msg00537.html


对于Windows的Git Bash(mingw64)也可以正常工作
Dolphin

1
${SSH_ENV}需要是"${SSH_ENV}"如果您的Windows用户名中有一个空间
rgvcorley

这个答案似乎在互联网上反弹。它在更大的stackoverflow线程上。最好/最简单的方法恕我直言。
卢克·戴维斯


6

我最近开始使用:

https://github.com/ccontavalli/ssh-ident

我要做的就是添加:

  alias ssh=/path/to/ssh-ident

在我的.bashrc文件中。该脚本负责:

  • 首次需要时创建代理
  • 按需加载必要的密钥
  • 在多个登录会话中共享代理
  • 管理多个代理,每个代理用于我在线使用的“身份”,并根据我要连接的主机或当前工作目录使用正确的代理。

ssh-ident太棒了!当我尝试ssh时,它会解锁密钥并加载代理,而不必提前解锁密钥。这使键超时很有用。更重要的是,它使我的代理分开以用于不同的用途(存在重大安全风险;一台计算机上的root具有我对当前代理具有密钥的所有其他计算机的访问级别!)
00prometheus

5

我宁愿使事情尽可能简单:(摘自~/.profile

check-ssh-agent() {
    [ -S "$SSH_AUTH_SOCK" ] && { ssh-add -l >& /dev/null || [ $? -ne 2 ]; }
}

# attempt to connect to a running agent
check-ssh-agent || export SSH_AUTH_SOCK="$(< ~/.tmp/ssh-agent.env)"
# if agent.env data is invalid, start a new one
check-ssh-agent || {
    eval "$(ssh-agent -s)" > /dev/null
    echo "$SSH_AUTH_SOCK" > ~/.tmp/ssh-agent.env
}

我以前没有考虑过使用-a,但是可能会更容易:

check-ssh-agent || export SSH_AUTH_SOCK=~/.tmp/ssh-agent.sock
check-ssh-agent || eval "$(ssh-agent -s -a ~/.tmp/ssh-agent.sock)" > /dev/null

真好 我在回答中对此做了一些简化(如下)。
以太2015年

2

就我而言,我在PowerShell中安装了posh-git,并希望cygwin使用相同的ssh-agent。我必须做一些路径操作,因为它们使用不同的tmp文件夹,并且创建的.env文件是带有BOM和CR \ LF的UTF16,因此处理起来很有趣。将以下内容添加到cygwin使用的.bashrc中应该可以工作:

# Connect to ssh-agent started by posh-git
SSH_AGENT_ENV=$(cygpath "$LOCALAPPDATA\Temp")
if [ -z $SSH_AUTH_SOCK ] && [ -z $SSH_TTY ]; then  # if no agent & not in ssh
  if [ -f "$SSH_AGENT_ENV/.ssh/SSH_AUTH_SOCK.env" ]; then
    AUTH_SOCK=$(iconv -c -f UTF-16LE -t US-ASCII "$SSH_AGENT_ENV/.ssh/SSH_AUTH_SOCK.env" | tr -d '\r\n')
    export SSH_AUTH_SOCK="${AUTH_SOCK/\/tmp/$SSH_AGENT_ENV}"
    ssh-add -l > /dev/null
    if [ $? = 2 ] ; then
      echo "Failed to setup posh-git ssh-agent using $AUTH_SOCK"
      unset SSH_AUTH_SOCK
    else
      echo "Found posh-git ssh-agent $AUTH_SOCK"
    fi
  else #Start new agent if you want (not shared by posh-git)
    echo "failed to setup posh-git ssh-agent"
    #eval `ssh-agent -s` > /dev/null
  fi
fi

1

再次提供一个示例,可以立即放入.bash_profile,并要求在登录时添加默认密钥。就我而言,转发不是一种选择。

do-ssh-agent() {
  # function to start the ssh-agent and store the agent details for later logon
  ssh-agent -s > ~/.ssh-agent.conf 2> /dev/null
  . ~/.ssh-agent.conf > /dev/null
}

# set time a key should be kept in seconds
keyage=3600

if [ -f ~/.ssh-agent.conf ] ; then
  . ~/.ssh-agent.conf > /dev/null
  ssh-add -l > /dev/null 2>&1
  # $?=0 means the socket is there and it has a key
  # $?=1 means the socket is there but contains no key
  # $?=2 means the socket is not there or broken
  stat=$?
  if [ $stat -eq 1 ] ; then
    ssh-add -t $keyage > /dev/null 2>&1
  elif [ $stat -eq 2 ] ; then
    rm -f $SSH_AUTH_SOCK
    do-ssh-agent
    ssh-add -t $keyage > /dev/null 2>&1
  fi
else
  do-ssh-agent
  ssh-add -t $keyage > /dev/null 2>&1
fi

1

这是我的解决方案,改编自https://superuser.com/a/141233/5255(在此线程中):

# attempt to connect to a running agent - cache SSH_AUTH_SOCK in ~/.ssh/
sagent()
{
    [ -S "$SSH_AUTH_SOCK" ] || export SSH_AUTH_SOCK="$(< ~/.ssh/ssh-agent.env)"

    # if cached agent socket is invalid, start a new one
    [ -S "$SSH_AUTH_SOCK" ] || {
        eval "$(ssh-agent)"
        ssh-add -t 25920000 -K ~/.ssh/id_rsa
        echo "$SSH_AUTH_SOCK" > ~/.ssh/ssh-agent.env
    }
}

1

创建文件〜/ ssh-agent.sh

agent_out_file="$HOME/ssh-agent.out"

function initialize {
    pgrep ssh-agent && kill $(pgrep ssh-agent)
    ssh-agent -s > $agent_out_file 
    . $agent_out_file
}

pgrep ssh-agent
if [ $? -eq 0 ]; then # ssh agent running
    ssh-add -l > /dev/null 2>&1
    status=$?
    if [ $status -eq 0 ]; then # can connect to ssh agent and keys available
        echo nothing to do
    elif [ $status -eq 1 ]; then # can connect to ssh agent and no keys available
        echo nothing to do
    elif [ $status -eq 2 ]; then # cannot connect to ssh agent
        . $agent_out_file
    fi
else # ssh agent not running
    initialize   
fi

将文件包含在.bashrc中

. ~/ssh-agent.sh

0

这是我添加的对我有用的东西。它首先检查您是否正在运行代理,如果是,它将为它设置合适的环境,否则将创建它。还消除了创建额外代理的麻烦:

只要把它放在你的 .bashrc

function start_agent() {
    killall ssh-agent  2> /dev/null
    ssh-agent | sed 's/ Agent pid//' > $SSH_ENV
    . $SSH_ENV > $SSH_PID_FILE
    ssh-add ~/.ssh/bb_readonly_rsa 2> /dev/null
}

mkdir -p "$HOME/.ssh/agent"
SSH_ENV="$HOME/.ssh/agent/env"
SSH_PID_FILE="$HOME/.ssh/agent/pid"

if [[ -e $SSH_PID_FILE ]]; then
    SSH_PID=$(< $SSH_PID_FILE) 
    PROCESS=$(ps -p $SSH_PID -o comm=)

    if [[ $PROCESS == 'ssh-agent' ]]; then
        . $SSH_ENV > $SSH_PID_FILE
    else 
        start_agent
    fi  
else
    start_agent
fi

0

我也从我的.bashrc直接获得了这个问题的变化形式:

# File for storing SSH agent information
OSH=".agent.${HOSTNAME}"

# Test if an agent file exists
if [ -f ${OSH} ];

    # We have one, so let's use it
    then eval `cat ${OSH}` >/dev/null

else

    # No file exists, so we must spawn a new agent
    eval `ssh-agent | tee ${OSH}` >/dev/null

fi

# Try to list agent keys
ssh-add -l &>/dev/null

# Determine the agent status
case $? in

    # Current and SSH keys installed, nothing to do here
    0) ;;

    # Current but no SSH keys installed, so we must add them
    1) ssh-add ;;

    # Stale, so we must redo from scratch with a new agent, then add keys
    *) eval `ssh-agent | tee ${OSH}` >/dev/null && ssh-add ;;

esac

此解决方案将SSH代理信息的副本存储在您的主目录中。如果您有一个NFS自动挂载的主目录,该目录可能在多个主机之间共享,则该主机名将用作文件名的一部分以区分它们,因此从一台计算机登录不会破坏另一台计算机上正在使用的代理文件。

行为:

1)首次用户会话被提示输入密钥密码。

2)第二,第三和第四(等)会话继承了在第一个会话中添加的SSH代理和密钥。

3)如果代理被杀死或崩溃,则随后的第一个会话将创建一个新的代理,用新的代理覆盖代理文件-并再次提示输入密钥密码。只要新的SSH代理保持运行,后续创建的会话的行为将与场景2)相似。


0

(这是指帖子2更高,我无法添加评论)

@raghavan:您的示例很有用,但建议更改包含以下内容的两行

pgrep ssh-agent

pgrep -u $ USER ssh-agent> / dev / null

因此,仅找到在当前用户下运行的代理,并且不会将pid回显到屏幕(更清洁)。

还将建议将$ HOME / ssh-agent.out更改为$ HOME / .ssh-agent.out

问候


0

我阅读了您的原始解决方案和一些建议的解决方案,但决定简化此过程以供自己使用。这是我在自己的.bashrc中添加的内容:

    # get active ssh-agent, or launch new
    SSH_AGENT_PID=$(ps -fC ssh-agent | grep "ssh-agent -a ${HOME}/.ssh/ssh-agent-socket" | awk '{print $2}')
    if [ -z "${SSH_AGENT_PID}" ]; then
      # If there is no ssh-agent running, we'll make sure one hasn't left a socket file dangling
      rm ${HOME}/.ssh/ssh-agent-socket &> /dev/null
      # And of course start one
      eval $(ssh-agent -a ${HOME}/.ssh/ssh-agent-socket)
    else
      # We found a process matching our requirements, so sticking with that
      export SSH_AGENT_PID
      export SSH_AUTH_SOCK="${HOME}/.ssh/ssh-agent-socket"
    fi

我在这里做了两个假设:

  • 〜/ .ssh目录存在。
  • 您在系统上每个用户只需要一个ssh-agent套接字。
  • 设置HOME环境变量(因为为什么不设置对吧?)。
  • 您将手动处理正在运行进程的情况,但是由于某种原因它不使用指定的套接字文件。

总而言之,我认为这是一个简单的解决方案。


0

我发现我经常有多个ssh-agent进程正在运行,并且套接字文件名中的PID实际上从未与运行中的PID匹配ssh-agent,因此基于上面的许多示例,我尝试了一些措施来尝试从这些条件中恢复。

它是一个函数,如果有用户ID,它将使用Zsh变量作为用户ID,并/tmp通过限制find(1)更多内容来尝试花费更少的时间来解析可能的大型目录。

它可能仍然容易出错,而且很复杂,但是一些粗略的测试表明它通常适用于我的用例,所以去了:

attach_ssh_agent(){
  如果[-n“ $ SSH_AGENT_PID”]; 然后
    ssh-add -l> / dev / null
    ret = $?
    如果[$ ret -ge 2]; 然后
      echo“代理pid $ SSH_AGENT_PID没用(ret = $ ret)-正在杀死...”
      杀死$ SSH_AGENT_PID
      未设置$ SSH_AGENT_PID
    elif [$ ret = 1]; 然后
      echo“代理pid $ SSH_AGENT_PID没什么用(ret = $ ret)-会播种...”
    其他
      回声“代理pid $ SSH_AGENT_PID”
      返回
    科幻
  科幻
  如果[-S“ $ SSH_AUTH_SOCK”]; 然后
    ssh-add -l> / dev / null
    ret = $?
    如果[$ ret = 2]; 然后
      echo“套接字$ SSH_AUTH_SOCK已死-正在删除...”
      rm -f $ SSH_AUTH_SOCK
      未设置SSH_AUTH_SOCK
    elif [$ ret = 1]; 然后
      echo“套接字$ SSH_AUTH_SOCK指向没有密钥的代理...”
      SSH添加
    其他
      回声“找到ssh代理$ SSH_AUTH_SOCK(ret = $ ret)”
      返回
    科幻
  科幻
  对于$(查找/ tmp / -mindepth 2 -maxdepth 2 -uid $ {UID:-$(id -u)} -path'/tmp/ssh-*/agent.*'-type s)中的sf; 做
    测试-r $ sf || 继续
    导出SSH_AUTH_SOCK = $ sf
    SSH_AGENT_PID = $(基本名称$ SSH_AUTH_SOCK | cut -d。-f2)
    #与其他流程叉一起比赛
    尝试= 50
    而[$ try -gt 0]; 做
      try = $((($ try-1))
      导出SSH_AGENT_PID = $((($ SSH_AGENT_PID + 1))
      回声“测试$ SSH_AUTH_SOCK-> $ SSH_AGENT_PID”
      ssh_agent_running = $(ps -u $ USER | grep ssh-agent)
      如果[-z“ $ ssh_agent_running”]; 然后
        echo“套接字$ SSH_AUTH_SOCK不包含任何正在运行的代理的链接-删除中...”
        rm -f $ SSH_AUTH_SOCK
        继续
      科幻
      如果回显“ $ ssh_agent_running” | \
           awk'$ 1 =='$ SSH_AGENT_PID'{
                  找到= 1;
                  退出(0);
              }
              结束 {
                  如果(!找到){
                      打印“找不到正在运行的PID'$ SSH_AGENT_PID'”;
                      出口(1);
                  }
              }'; 然后
        ssh-add -l> / dev / null
        ret = $?
        如果[$ ret -ge 2]; 然后
          echo“套接字$ SSH_AUTH_SOCK在$ SSH_AGENT_PID处不包含指向有用代理的链接-删除中...”
          rm -f $ SSH_AUTH_SOCK
          杀死$ SSH_AGENT_PID
          未设置SSH_AGENT_PID
          继续2
        elif [$ ret = 1]; 然后
          echo“套接字$ SSH_AUTH_SOCK在$ SSH_AGENT_PID处包含一个用处不大的代理的链接-播种...”
          SSH添加
          如果!ssh-add -l> / dev / null; 然后
            echo“套接字$ SSH_AUTH_SOCK仍然包含指向$ SSH_AGENT_PID处不那么有用的代理的链接-正在中止。”
            返回
          其他
            打破
          科幻
        其他
          打破
        科幻
      其他
#echo“无法将套接字$ SSH_AUTH_SOCK与代理PID $ SSH_AGENT_PID匹配-正在跳过...”
        继续
      科幻
    做完了
    如果[$ try -gt 0]; 然后
      回声“找到ssh代理$ SSH_AUTH_SOCK”
      回声“代理pid $ SSH_AGENT_PID”
      返回
    科幻
  做完了
  如果[-n“ $ try” -a -n“ $ SSH_AUTH_SOCK” -a -n“ $ ssh_agent_running”];然后
    echo“我们尝试了很多次,但是无法将$ SSH_AUTH_SOCK与任何正在运行的代理匹配,叹气”
    回声“ $ ssh_agent_running”
    echo“将这些剩余的东西留在后面并启动新的代理...”
  科幻
  评估$(ssh-agent -t 28800)
  SSH添加
}

0

这是我对此的看法。I“ ”下面的脚本从我的.bash_profile

MYAGENTS=(`pgrep -U $USER -f ^ssh-agent$|sort -n`)

echo "Found ${#MYAGENTS[@]} ssh-agents."

# Let's try to take over the agents, like we do everynight Pinky!
if [[ "${MYAGENTS[@]}" ]];then
  KEEPER=${MYAGENTS[0]}
  echo KEEPER: $KEEPER
  OUTCAST=${MYAGENTS[@]:1}
  [[ "$OUTCAST" ]] && { echo "Goodbye agent $OUTCAST"; kill $OUTCAST; }
  SSH_AUTH_SOCK=`awk '/tmp\/ssh/ {print $NF}' /proc/$KEEPER/net/unix`
  export SSH_AUTH_SOCK;
  SSH_AGENT_PID=$KEEPER; export SSH_AGENT_PID;
else
  NEWAGENT="`ssh-agent`"
  echo $NEWAGENT;
  eval $NEWAGENT
fi

ssh-add -l | grep "The agent has no identities" && ssh-add

0

这是一个简单的脚本,该脚本将始终重复使用相同的ssh-agent,或者在未运行时启动ssh-agent。关键是使用该-a选项以使用相同的套接字名称。否则,默认情况下,它将每次都选择一个随机的套接字名称。您也可以轻松地将这3行合并为1行别名。

# set SSH_AUTH_SOCK env var to a fixed value
export SSH_AUTH_SOCK=~/.ssh/ssh-agent.sock

# test whether $SSH_AUTH_SOCK is valid
ssh-add -l 2>/dev/null >/dev/null

# if not valid, then start ssh-agent using $SSH_AUTH_SOCK
[ $? -ge 2 ] && ssh-agent -a "$SSH_AUTH_SOCK" >/dev/null

资源

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.