Skip to content

Commit

Permalink
Merge pull request #6 from JuliaWeb/general-updates
Browse files Browse the repository at this point in the history
General updates and bug fixes
  • Loading branch information
JamesWrigley authored Feb 28, 2024
2 parents 8250912 + 510279b commit ce815b5
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 42 deletions.
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

0 comments on commit ce815b5

Please sign in to comment.