refactor: replace gliderlabs/ssh with golang.org/x/crypto/ssh (#38837)

Migrate away from this thin ssh wrapper module while adding more test
coverage.

Removes `sessionPartial`, which hand-copied the layout of a private
`gliderlabs/ssh` struct and reinterpreted a pointer to it via
`reflect.UnsafePointer` to reach the permissions of the authenticated
connection. The layout is unchecked, so an upstream field reorder would
mismatch silently.

The builtin server only needs the session channel with `exec` and
`shell`. Serving those on `x/crypto` drops the hack and the dependency,
since `PublicKeyCallback` returns permissions per key and `x/crypto`
assigns them only after verifying the signature.

Two benign behavior changes:

1. Internal session handler errors report exit status 1 rather than 0,
so a client no longer reads a failure as success.
1. An unusable host key is fatal at startup instead of being replaced by
an ephemeral one that would trigger an error at the client.
This commit is contained in:
silverwind
2026-08-09 13:32:50 +02:00
committed by GitHub
parent 79535f4e01
commit ecbef41c06
7 changed files with 260 additions and 174 deletions
+38
View File
@@ -4,19 +4,25 @@
package integration
import (
"bytes"
"fmt"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"testing"
"time"
auth_model "gitea.dev/models/auth"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
gossh "golang.org/x/crypto/ssh"
)
func doCheckRepositoryEmptyStatus(ctx APITestContext, isEmpty bool) func(*testing.T) {
@@ -42,6 +48,38 @@ func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) {
}
}
// TestSSHShellWelcome covers the "shell" request, which carries no command payload unlike "exec"
func TestSSHShellWelcome(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteUser)
withKeyFile(t, "welcome-key", func(keyFile string) {
t.Run("CreateUserKey", doAPICreateUserKey(ctx, "welcome-key", keyFile))
privateKey, err := os.ReadFile(keyFile)
require.NoError(t, err)
signer, err := gossh.ParsePrivateKey(privateKey)
require.NoError(t, err)
client, err := gossh.Dial("tcp", net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort)), &gossh.ClientConfig{
User: setting.SSH.BuiltinServerUser,
Auth: []gossh.AuthMethod{gossh.PublicKeys(signer)},
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
})
require.NoError(t, err)
defer client.Close()
session, err := client.NewSession()
require.NoError(t, err)
var stderr bytes.Buffer
session.Stderr = &stderr // "gitea serv" writes the welcome with println, which goes to stderr
require.NoError(t, session.Shell())
require.NoError(t, session.Wait()) // fails unless the server reports exit status 0
assert.Contains(t, stderr.String(), "You've successfully authenticated with the key named welcome-key")
})
})
}
func TestPushDeployKeyOnEmptyRepo(t *testing.T) {
onGiteaRun(t, testPushDeployKeyOnEmptyRepo)
}