我承诺提供我的示例,下面是内容。基本上,我的解决方案通过ssh隧道连接到远程服务器,并通过该隧道查询远程数据库。ssh隧道是解决方案的一部分。
我需要做的第一件事是将我的PuTTY .ppk私钥文件转换为有效的OpenSSH .pem密钥文件。这可以使用PuTTYgen中的导出功能轻松完成。由于我想支持密码加密的私钥,我还需要一个函数来解密密钥并将其从解密后的原始格式重新格式化为golang.org/x/crypto/ssh/ParsePrivateKey接受的有效格式,该格式用于获取签名者列表以进行身份验证。
解决方案本身包含在两个文件中的软件包中。应用程序的主要部分在main.go中完成,其中包含所有相关数据分配以及与数据库查询相关的代码。与ssh隧道和密钥处理有关的所有内容都包含在sshTunnel.go中。
该解决方案不提供安全密码存储机制,也不要求输入密码。密码在代码中提供。但是,实现密码请求的回调方法并不太复杂。
请注意:从性能角度来看,这不是理想的解决方案。它还缺乏适当的错误处理。我提供这个作为示例。
这个示例是一个经过测试并且可行的示例。我从一台Windows 8.1电脑上开发并使用它。数据库服务器在远程Linux系统上。您只需要更改main.go中的数据和查询部分即可。
以下是包含在main.go中的第一部分:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"os"
)
const (
sshServerHost = "test.example.com"
sshServerPort = 22
sshUserName = "tester"
sshPrivateKeyFile = "testkey.pem"
sshKeyPassphrase = "testoster0n"
sshLocalHost = "localhost"
sshLocalPort = 9000
sshRemoteHost = "127.0.0.1"
sshRemotePort = 3306
mySqlUsername = "opensim"
mySqlPassword = "h0tgrits"
mySqlDatabase = "opensimdb"
)
func main() {
fmt.Println("-> mysqlSSHtunnel")
tunnel := sshTunnel()
go tunnel.Start()
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
mySqlUsername, mySqlPassword, sshLocalHost, sshLocalPort, mySqlDatabase)
db, err := sql.Open("mysql", dsn)
if err != nil {
dbErrorHandler(err)
}
defer db.Close()
rows, err := db.Query("SELECT * FROM migrations")
if err != nil {
dbErrorHandler(err)
}
defer rows.Close()
for rows.Next() {
var version int
var name string
if err := rows.Scan(&name, &version); err != nil {
dbErrorHandler(err)
}
fmt.Printf("%s, %d\n", name, version)
}
if err := rows.Err(); err != nil {
dbErrorHandler(err)
}
fmt.Println("<- mysqlSSHtunnel")
}
func dbErrorHandler(err error) {
switch err := err.(type) {
default:
fmt.Printf("Error %s\n", err)
os.Exit(-1)
}
}
现在是 sshTunnel.go 的第二部分:
package main
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"golang.org/x/crypto/ssh"
"io"
"io/ioutil"
"net"
)
type Endpoint struct {
Host string
Port int
}
func (endpoint *Endpoint) String() string {
return fmt.Sprintf("%s:%d", endpoint.Host, endpoint.Port)
}
type SSHtunnel struct {
Local *Endpoint
Server *Endpoint
Remote *Endpoint
Config *ssh.ClientConfig
}
func (tunnel *SSHtunnel) Start() error {
listener, err := net.Listen("tcp", tunnel.Local.String())
if err != nil {
return err
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
return err
}
go tunnel.forward(conn)
}
}
func (tunnel *SSHtunnel) forward(localConn net.Conn) {
serverConn, err := ssh.Dial("tcp", tunnel.Server.String(), tunnel.Config)
if err != nil {
fmt.Printf("Server dial error: %s\n", err)
return
}
remoteConn, err := serverConn.Dial("tcp", tunnel.Remote.String())
if err != nil {
fmt.Printf("Remote dial error: %s\n", err)
return
}
copyConn := func(writer, reader net.Conn) {
_, err := io.Copy(writer, reader)
if err != nil {
fmt.Printf("io.Copy error: %s", err)
}
}
go copyConn(localConn, remoteConn)
go copyConn(remoteConn, localConn)
}
func DecryptPEMkey(buffer []byte, passphrase string) []byte {
block, _ := pem.Decode(buffer)
der, err := x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
fmt.Println("decrypt failed: ", err)
}
encoded := base64.StdEncoding.EncodeToString(der)
encoded = "-----BEGIN RSA PRIVATE KEY-----\n" + encoded +
"\n-----END RSA PRIVATE KEY-----\n"
return []byte(encoded)
}
func PublicKeyFile(file string, passphrase string) ssh.AuthMethod {
buffer, err := ioutil.ReadFile(file)
if err != nil {
return nil
}
if bytes.Contains(buffer, []byte("ENCRYPTED")) {
buffer = DecryptPEMkey(buffer, passphrase)
}
signers, err := ssh.ParsePrivateKey(buffer)
if err != nil {
return nil
}
return ssh.PublicKeys(signers)
}
func sshTunnel() *SSHtunnel {
localEndpoint := &Endpoint{
Host: sshLocalHost,
Port: sshLocalPort,
}
serverEndpoint := &Endpoint{
Host: sshServerHost,
Port: sshServerPort,
}
remoteEndpoint := &Endpoint{
Host: sshRemoteHost,
Port: sshRemotePort,
}
sshConfig := &ssh.ClientConfig{
User: sshUserName,
Auth: []ssh.AuthMethod{
PublicKeyFile(sshPrivateKeyFile, sshKeyPassphrase)},
}
return &SSHtunnel{
Config: sshConfig,
Local: localEndpoint,
Server: serverEndpoint,
Remote: remoteEndpoint,
}
}
localhost:3306
作为连接端点,而不是remoteserver:3306
。请注意,只有当你的SSH隧道在本地主机上侦听并且TCP端口3306上才是真实的。 - kostix