Documentation
¶
Overview ¶
Package protocol carries the smart-HTTP and (later) smart-SSH bits of the git wire protocol that we generate ourselves. The bulk of the protocol — capability negotiation, want/have, multi-ack, side-band — stays inside canonical `git`'s `upload-pack` and `receive-pack`. We only emit the framing we need to wrap their advertise-refs output for HTTP transport.
Index ¶
- Constants
- Variables
- func Cmd(ctx context.Context, svc Service, gitDir string, advertiseRefs bool, ...) *exec.Cmd
- func DrainStderr(cmd *exec.Cmd) func() []byte
- func FriendlyMessageFor(err error, requestID string) string
- func ParseRemoteIP(sshConnection string) string
- func PrepareDispatch(ctx context.Context, deps SSHDispatchDeps, in SSHDispatchInput) (*SSHDispatchResult, ParsedSSHCommand, error)
- func WriteFlush(w io.Writer) error
- func WritePkt(w io.Writer, payload string) error
- func WriteServiceAdvertisement(w io.Writer, service string) error
- type ParsedSSHCommand
- type SSHDispatchDeps
- type SSHDispatchInput
- type SSHDispatchResult
- type Service
Constants ¶
const ( MsgRepoNotFound = "shithub: repository not found" MsgPermDenied = "shithub: permission denied" MsgArchived = "shithub: this repository is archived; pushes are disabled" MsgSuspended = "shithub: your account is suspended" )
Friendly stderr messages — these are what the user sees in their terminal when `git clone` fails. Keep them actionable.
const FlushPkt = "0000"
FlushPkt is the literal pkt-line sequence that says "no more packets." git uses it to terminate sub-streams.
const MaxPktLine = 65520
MaxPktLine is git's hard limit for a single packet: 65520 bytes of payload + 4 bytes of length prefix. Anything longer is malformed.
Variables ¶
var ( ErrSSHRepoNotFound = errors.New("ssh dispatch: repo not found") ErrSSHPermDenied = errors.New("ssh dispatch: permission denied") ErrSSHArchived = errors.New("ssh dispatch: archived") ErrSSHSuspended = errors.New("ssh dispatch: suspended") ErrSSHInternal = errors.New("ssh dispatch: internal error") )
Sentinel errors callers can errors.Is to map to friendly messages.
var ErrInvalidSSHPath = errors.New("ssh: invalid repo path")
ErrInvalidSSHPath is returned when the parsed path fails owner/repo validation. This is the path-traversal defense.
var ErrSubprocessFailed = errors.New("git subprocess exited non-zero")
ErrSubprocessFailed is returned by Run when the git subprocess exits non-zero. The captured stderr is wrapped via %s so the operator sees it in logs without it being escaped through the chain.
var ErrUnknownSSHCommand = errors.New("ssh: unsupported command")
ErrUnknownSSHCommand is returned when SSH_ORIGINAL_COMMAND doesn't match the strict whitelist. Callers should surface a friendly "shithub does not allow shell access" message and exit non-zero.
Functions ¶
func Cmd ¶
func Cmd(ctx context.Context, svc Service, gitDir string, advertiseRefs bool, extraEnv []string) *exec.Cmd
Cmd builds an *exec.Cmd that drives the requested service against the bare repo at gitDir. AdvertiseRefs flips on the --advertise-refs flag used by the info/refs response. extraEnv is appended to os.Environ() — the caller uses it to thread SHITHUB_USER_ID/etc. through git's environment so post-receive hooks can read them.
Subprocess lifecycle:
- Killed on ctx cancel (Go ≥ 1.20: cmd.Cancel default kills with SIGKILL; we tighten to a 250ms WaitDelay so a stuck subprocess doesn't pin a worker forever).
- Stderr is drained by the helper Drain() so the OS pipe buffer doesn't fill and deadlock the caller's stdout copy. Captured stderr is exposed via Stderr() on the returned Process.
func DrainStderr ¶
DrainStderr starts a goroutine that copies stderr into a bounded ring buffer; returns a function the caller invokes after Wait() to get the captured bytes. The cap is `stderrCap`; further writes are silently discarded.
Always call this BEFORE Start() and call the returned closure AFTER Wait() to avoid the deadlock where a verbose stderr fills the OS pipe.
func FriendlyMessageFor ¶
FriendlyMessageFor returns the user-facing stderr line for a typed dispatch error. Unknown errors collapse to a generic "internal error (request_id=...)" message — never leak the underlying cause.
func ParseRemoteIP ¶
ParseRemoteIP extracts the connecting client's IP from the SSH_CONNECTION env var ("<client> <cport> <server> <sport>"). Returns "" when malformed; callers may pass the empty string through to the hook env.
func PrepareDispatch ¶
func PrepareDispatch(ctx context.Context, deps SSHDispatchDeps, in SSHDispatchInput) (*SSHDispatchResult, ParsedSSHCommand, error)
PrepareDispatch is the brains of the SSH-shell command. It parses SSH_ORIGINAL_COMMAND, resolves user + repo, runs the inline owner- only authorization, and builds the env + argv the caller needs to exec git. It does NOT call exec itself — the caller does, after closing the DB pool (syscall.Exec preserves all open FDs and we don't want a leaked Postgres connection).
func WriteFlush ¶
WriteFlush emits the flush packet (`0000`). Marks the end of a sub-stream like the service advertisement preamble.
func WritePkt ¶
WritePkt writes one pkt-line packet — a 4-hex-digit length prefix (covering payload + 4) followed by the payload. Exposed so the smart- HTTP info/refs handler can prepend its `# service=...` advertisement.
func WriteServiceAdvertisement ¶
WriteServiceAdvertisement writes the standard preamble that the smart- HTTP info/refs response uses to tell git which service is being advertised. The wire format is:
001e# service=git-upload-pack\n0000 └┬┘└──────────┬─────────────┘└─┬─┘ │ │ └ flush │ └ payload (trailing newline included) └ length prefix (hex)
Then `git upload-pack --advertise-refs --stateless-rpc <repo>` writes its actual ref advertisement after this.
Types ¶
type ParsedSSHCommand ¶
type ParsedSSHCommand struct {
Service Service // UploadPack or ReceivePack
Owner string // already lowercase, validated via the same shape rules as RepoFS
Repo string // already lowercase, validated via repo-name rules
}
ParsedSSHCommand is the validated shape of `SSH_ORIGINAL_COMMAND`. The dispatcher resolves Owner+Repo against the DB and feeds Service to the same exec helper used by the HTTP transport.
func ParseSSHCommand ¶
func ParseSSHCommand(cmd string) (ParsedSSHCommand, error)
ParseSSHCommand strict-parses SSH_ORIGINAL_COMMAND into Service + Owner + Repo. Returns ErrUnknownSSHCommand for anything that isn't a `git-{upload,receive}-pack <path>` invocation, and ErrInvalidSSHPath when the path fails the storage whitelist.
Caller is expected to have already trimmed surrounding whitespace if any; we additionally enforce no leading/trailing whitespace ourselves.
type SSHDispatchDeps ¶
SSHDispatchDeps wires the dispatcher. The DB pool is sized small at the call site (max 2 conns) — every SSH connection runs through here and the latency floor matters.
type SSHDispatchInput ¶
type SSHDispatchInput struct {
OriginalCommand string
UserID int64
RemoteIP string // parsed from SSH_CONNECTION first field
}
SSHDispatchInput is everything that comes from the OS environment at dispatch time. The cobra command in cmd/shithubd/ssh.go reads SSH_ORIGINAL_COMMAND / SSH_CONNECTION / argv and passes them in.
type SSHDispatchResult ¶
SSHDispatchResult is what the caller needs to syscall.Exec the right git plumbing binary. Argv0 is the canonical binary path resolved from PATH; Argv0Args is just `[Argv0, GitDir]` (git-upload-pack and git-receive-pack each take exactly one positional argument). Env is the full environment for the new process — already includes PATH and the SHITHUB_* vars.