Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
Kerberos_krb5_jll = "b39eb1a6-c29a-53d7-8c32-632cd16f18da"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
libssh_jll = "a8d4f100-aa25-5708-be18-96e0805c2c9d"
Expand All @@ -17,6 +18,7 @@ CEnum = "0.4, 0.5"
Dates = "1"
DocStringExtensions = "0.9"
FileWatching = "1"
Kerberos_krb5_jll = "1"
Printf = "1"
Sockets = "1"
julia = "1.9"
Expand Down
26 changes: 26 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ CurrentModule = LibSSH
This documents notable changes in LibSSH.jl. The format is based on [Keep a
Changelog](https://keepachangelog.com).

## Unreleased

### Added

- It's possible to set an interface for the [`Forwarder`](@ref) socket to listen
on with the `localinterface` argument ([#6]).
- A new `Gssapi` module to help with [GSSAPI support](@ref). In particular,
[`Gssapi.principal_name()`](@ref) was added to get the name of the default
principal if one is available ([#6]).

### Changed

- The `userauth_*` functions will now throw a `LibSSHException` by default if
they got a `AuthStatus_Error` from libssh. This can be disabled by passing
`throw_on_error=false` ([#6]).
- `gssapi_available()` was renamed to [`Gssapi.isavailable()`](@ref) ([#6]).

### Fixed

- Fixed some race conditions in [`poll_loop()`](@ref) and
- [`Base.run(::Cmd, ::Session)`](@ref) now properly converts commands into
strings before executing them remotely, previously things like quotes weren't
escaped properly ([#6]).
- Fixed a bug in [`Base.run(::Cmd, ::Session)`](@ref) that would clear the
output buffer when printing ([#6]).

## [v0.2.1] - 2024-02-27

### Added
Expand Down
8 changes: 6 additions & 2 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
```@meta
CurrentModule = LibSSH
```

# LibSSH.jl

This package provides a high-level API and low-level bindings to
Expand Down Expand Up @@ -32,8 +36,8 @@ pkg> add LibSSH

## Limitations

- GSSAPI support is disabled on Windows and macOS due to `Kerberos_krb5_jll` not
being available on those platforms.
- GSSAPI support isn't available on all platforms (see
[`Gssapi.isavailable`](@ref)).
- Many features don't have high-level wrappers (see [Contributing](@ref)).

## FAQ
Expand Down
8 changes: 7 additions & 1 deletion docs/src/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ Depth = 10
```@docs
lib_version
get_hexa
gssapi_available
```

## GSSAPI support

```@docs
Gssapi.isavailable
Gssapi.principal_name
```

## Messages
Expand Down
10 changes: 1 addition & 9 deletions src/LibSSH.jl
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,6 @@ function lib_version()
VersionNumber(lib.LIBSSH_VERSION_MAJOR, lib.LIBSSH_VERSION_MINOR, lib.LIBSSH_VERSION_MICRO)
end

"""
$(TYPEDSIGNATURES)

Check if GSSAPI support is available (currently only Linux and FreeBSD).
"""
function gssapi_available()
Sys.islinux() || Sys.isfreebsd()
end

# Safe wrapper around poll_fd(). There's a race condition in older Julia
# versions between the loop condition evaluation and this line, so we wrap
# poll_fd() in a try-catch in case the bind (and thus the file descriptor) has
Expand All @@ -185,6 +176,7 @@ function _safe_poll_fd(args...; kwargs...)
return result
end

include("gssapi.jl")
include("pki.jl")
include("callbacks.jl")
include("session.jl")
Expand Down
43 changes: 34 additions & 9 deletions src/channel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,10 @@ $(TYPEDSIGNATURES)

Poll a (owning) channel in a loop while it's alive, which will trigger any
callbacks. This function should always be called on a channel for it to work
properly. It will return the last result from [`lib.ssh_channel_poll()`](@ref),
which should be checked to see if it's `SSH_EOF`.
properly. It will return:
- `nothing` if the channel was closed during the loop.
- Otherwise the last result from [`lib.ssh_channel_poll()`](@ref), which should
be checked to see if it's `SSH_EOF`.
"""
function poll_loop(sshchan::SshChannel)
if !sshchan.owning
Expand All @@ -330,6 +332,13 @@ function poll_loop(sshchan::SshChannel)

ret = SSH_ERROR
while true
# We always check if the channel and session are open within the loop
# because ssh_channel_poll() will execute callbacks, which could close
# them before returning.
if !isopen(sshchan)
return nothing
end

# Note that we don't actually read any data in this loop, that's
# handled by the callbacks, which are called by ssh_channel_poll().
ret = lib.ssh_channel_poll(sshchan.ptr, 0)
Expand All @@ -339,6 +348,10 @@ function poll_loop(sshchan::SshChannel)
break
end

if !isopen(sshchan.session)
return nothing
end

wait(sshchan.session)
end

Expand Down Expand Up @@ -383,7 +396,7 @@ $(TYPEDFIELDS)
This is analogous to `Base.Process`, it represents a command running over an
SSH session. The stdout and stderr output are stored as byte arrays in
`SshProcess.out` and `SshProcess.err` respectively. They can be converted to
strings using e.g. `String(process.out)`.
strings using e.g. `String(copy(process.out))`.
"""
@kwdef mutable struct SshProcess
out::Vector{UInt8} = Vector{UInt8}()
Expand Down Expand Up @@ -441,7 +454,7 @@ end
function _exec_command(process::SshProcess)
sshchan = process._sshchan
session = sshchan.session
cmd_str = join(process.cmd.exec, " ")
cmd_str = Base.shell_escape(process.cmd)

# Open the session channel
ret = _session_trywait(session) do
Expand Down Expand Up @@ -552,7 +565,7 @@ function Base.run(cmd::Cmd, session::Session;
Base.wait(process._task)

if print_out
print(String(process.out))
print(String(copy(process.out)))
end
end

Expand Down Expand Up @@ -611,8 +624,10 @@ function _on_client_channel_eof(session, sshchan, client)
_logcb(client, "EOF")

close(client.sshchan)
closewrite(client.sock)
close(client.sock)
if isopen(client.sock)
closewrite(client.sock)
close(client.sock)
end
end

function _on_client_channel_close(session, sshchan, client)
Expand Down Expand Up @@ -715,9 +730,19 @@ mutable struct Forwarder
Create a `Forwarder` object to forward data from `localport` to
`remotehost:remoteport`. This will handle an internal [`SshChannel`](@ref)
for forwarding.

# Arguments
- `session`: The session to create a forwarding channel over.
- `localport`: The local port to bind to.
- `remotehost`: The remote host.
- `remoteport`: The remote port to bind to.
- `verbose`: Print logging messages on callbacks etc (not equivalent to
setting `log_verbosity` on a [`Session`](@ref)).
- `localinterface=IPv4(0)`: The interface to bind `localport` on.
"""
function Forwarder(session::Session, localport::Int, remotehost::String, remoteport::Int; verbose=false)
listen_server = Sockets.listen(IPv4(0), localport)
function Forwarder(session::Session, localport::Int, remotehost::String, remoteport::Int;
verbose=false, localinterface::Sockets.IPAddr=IPv4(0))
listen_server = Sockets.listen(localinterface, localport)

self = new(remotehost, remoteport, localport,
listen_server, nothing, _ForwardingClient[],
Expand Down
133 changes: 133 additions & 0 deletions src/gssapi.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
module Gssapi

using DocStringExtensions
import Kerberos_krb5_jll: libgssapi_krb5

import ..LibSSH as ssh


const krb5_context = Ptr{Cvoid}
const krb5_ccache = Ptr{Cvoid}
const krb5_principal = Ptr{Cvoid}

"""
$(TYPEDSIGNATURES)

Check if GSSAPI support is available. Currently this is only available on Linux
and FreeBSD because it's difficult to cross-compile `Kerberos_krb5_jll` for
other platforms (which is what we depend on for GSSAPI).
"""
function isavailable()
Sys.islinux() || Sys.isfreebsd()
end

mutable struct Krb5Context
ptr::Union{krb5_context, Nothing}

function Krb5Context()
context_ref = Ref{krb5_context}()
ret = @ccall libgssapi_krb5.krb5_init_context(context_ref::Ptr{krb5_context})::Cint
if ret != 0
error("Error initializing Kerberos context: $(ret)")
end

self = new(context_ref[])
finalizer(self) do context
@ccall libgssapi_krb5.krb5_free_context(context.ptr::krb5_context)::Cvoid
context.ptr = nothing
end
end
end

mutable struct Krb5Ccache
ptr::Union{krb5_ccache, Nothing}
context::Krb5Context

function Krb5Ccache(context::Krb5Context)
cache_ref = Ref{krb5_ccache}()
ret = @ccall libgssapi_krb5.krb5_cc_default(context.ptr::krb5_context,
cache_ref::Ptr{krb5_ccache})::Cint
if ret != 0
error("Error initializing default Kerberos cache: $(ret)")
end

self = new(cache_ref[], context)
finalizer(self) do cache
@ccall libgssapi_krb5.krb5_cc_close(cache.context.ptr::krb5_context,
cache.ptr::krb5_ccache)::Cint
cache.ptr = nothing
end
end
end

mutable struct Krb5Principle
ptr::Union{krb5_principal, Nothing}
context::Krb5Context

function Krb5Principle(context::Krb5Context, cache::Krb5Ccache)
principal_ref = Ref{krb5_principal}()
ret = @ccall libgssapi_krb5.krb5_cc_get_principal(context.ptr::krb5_context,
cache.ptr::krb5_ccache,
principal_ref::Ptr{krb5_principal})::Cint
if ret != 0
error("Error retrieving default principal: $(ret)")
end

self = new(principal_ref[], context)
finalizer(self) do principal
@ccall libgssapi_krb5.krb5_free_principal(principal.context.ptr::krb5_context,
principal.ptr::krb5_principal)::Cvoid
principal.ptr = nothing
end
end
end

function krb5_unparse_name(context::Krb5Context, principal::Krb5Principle)
name_ref = Ref{Cstring}()
ret = @ccall libgssapi_krb5.krb5_unparse_name(context.ptr::krb5_context,
principal.ptr::krb5_principal,
name_ref::Ptr{Cstring})::Cint
if ret != 0
error("Error getting principal name: $(ret)")
end

name = unsafe_string(name_ref[])
@ccall libgssapi_krb5.krb5_free_unparsed_name(context.ptr::krb5_context,
name_ref[]::Cstring)::Cvoid

return name
end

"""
$(TYPEDSIGNATURES)

Returns the name of the default principal from the default credential cache, or
`nothing` if a principal with a valid ticket was not found. This can be used to
check if [`ssh.userauth_gssapi()`](@ref) can be called. Under the hood it uses:
- [`krb5_cc_default()`](https://web.mit.edu/kerberos/krb5-1.18/doc/appdev/refs/api/krb5_cc_default.html)
- [`krb5_cc_get_principal()`](https://web.mit.edu/kerberos/krb5-1.18/doc/appdev/refs/api/krb5_cc_get_principal.html)

# Throws
- `ErrorException`: If GSSAPI support is not available on the current platform
(see [`isavailable()`](@ref)).
"""
function principal_name()
if !isavailable()
error("GSSAPI support not available, cannot get the principal name")
end

context = Krb5Context()
cache = Krb5Ccache(context)

# This will throw if a principal with a valid ticket doesn't exist
principal = nothing
try
principal = Krb5Principle(context, cache)
catch
return nothing
end

return krb5_unparse_name(context, principal)
end

end
Loading