API-Docker

 view release on metacpan or  search on metacpan

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

      Proto    => 'tcp',
      $timeout ? (Timeout => $timeout) : (),
      %ssl,
    );
    unless ($sock) {
      $self->_croak_connect_timeout($pending, $addr . ':' . $port . ' (TLS)')
        if $self->_connect_expired($timeout);
      # $SSL_ERROR carries the handshake failure -- an untrusted certificate,
      # a name that does not match -- and $! the plain connect failure. Both
      # are named because either can be the one that happened. The pragma is
      # for the package variable of a module that is not loaded at compile
      # time, which perl would otherwise report as a probable typo.
      no warnings 'once';
      croak 'Cannot connect to ' . $addr . ':' . $port . ' over TLS: '
        . ($IO::Socket::SSL::SSL_ERROR || $! || 'unknown error');
    }
    return $sock;
  }
  else {
    croak "Unsupported host format: $host (expected unix:// or tcp://)";
  }
}

# Loaded here rather than with the other modules at the top of the file.
# IO::Socket::SSL pulls in Net::SSLeay, which is XS compiled against libssl,
# and the unix:// transport -- local Docker, rootless Podman, the default and
# the only one most installations use -- never needs a byte of it. A hard
# dependency would make this client unbuildable on a machine with no OpenSSL
# headers for the sake of a transport it is not using, so it is a recommended
# one and this is the point where its absence becomes an error.
sub _load_ssl {
  my ($self) = @_;

  return 1 if eval { require IO::Socket::SSL; 1 };
  my $why = $@ || 'unknown error';
  $why =~ s/\s+\z//;
  croak __PACKAGE__ . ': tls => 1 needs IO::Socket::SSL, which failed to '
    . 'load (' . $why . '). It is a recommended rather than a required '
    . 'dependency because the unix:// transport never uses it -- install it '
    . 'with `cpanm IO::Socket::SSL` (or `cpanm --with-recommends '
    . 'API::Docker`)';
}

# The IO::Socket::SSL arguments for this client, as a plain hash, so the
# policy can be read off without opening a connection.
sub _ssl_options {
  my ($self, $addr) = @_;

  $self->_load_ssl;

  # SNI, sent whether or not the certificate is checked: a terminator serving
  # several names needs it to pick the right one, and that is true of an
  # unverified connection too.
  my %ssl = ( SSL_hostname => $addr );

  if ($self->tls_insecure) {
    # Everything below is off deliberately, and the attribute that got us here
    # says so in its name. Encryption without verification stops a passive
    # listener and nothing else: whoever answers the connection chooses the
    # certificate, so anyone able to redirect it reads and rewrites the
    # traffic -- credentials, image contents, container commands.
    $ssl{SSL_verify_mode}     = IO::Socket::SSL::SSL_VERIFY_NONE();
    $ssl{SSL_verifycn_scheme} = undef;
  }
  else {
    $ssl{SSL_verify_mode}     = IO::Socket::SSL::SSL_VERIFY_PEER();
    # The name is checked against the certificate as well as the chain: a
    # valid certificate for some other host is not this host.
    $ssl{SSL_verifycn_scheme} = 'http';
    $ssl{SSL_verifycn_name}   = $addr;
  }

  return (%ssl, $self->_ssl_certificates);
}

# cert.pem, key.pem and ca.pem in one directory -- the layout the docker CLI
# writes and the one cert_path has always pointed at, whether or not anything
# read it.
sub _ssl_certificates {
  my ($self) = @_;

  my $dir = $self->cert_path;
  return () unless defined $dir && length $dir;

  # cert_path defaults from DOCKER_CERT_PATH, so it can arrive from a machine's
  # environment rather than from this caller -- but it is only looked at once
  # TLS was asked for, and at that point a path naming nothing is a mistake
  # worth stopping on rather than quietly connecting without the certificates
  # the caller believes are in use.
  my $path = path($dir);
  croak __PACKAGE__ . ": cert_path $dir is not a directory. TLS expects the "
    . 'layout the docker CLI writes -- ca.pem, cert.pem and key.pem in one '
    . 'directory -- and this names nothing that could hold it'
    unless $path->is_dir;

  my %ssl;

  # No ca.pem is not an error: verifying a daemon behind a terminator with a
  # publicly trusted certificate needs no private trust anchor, and the
  # default store is then the right one. See L</"TLS on a tcp:// connection">.
  my $ca = $path->child('ca.pem');
  $ssl{SSL_ca_file} = "$ca" if $ca->exists;

  my $cert = $path->child('cert.pem');
  my $key  = $path->child('key.pem');
  my @half = grep { !$_->[1]->exists }
    ( [ 'cert.pem', $cert ], [ 'key.pem', $key ] );

  # One of the two is never a mode, only ever an accident: a key with no
  # certificate proves nothing and a certificate with no key cannot be used.
  croak __PACKAGE__ . ': cert_path ' . $dir . ' has ' . $half[0][0]
    . ' missing while the other half of the client certificate is there. '
    . 'Both cert.pem and key.pem are needed, or neither'
    if @half == 1;

  if (!@half) {
    $ssl{SSL_cert_file} = "$cert";
    $ssl{SSL_key_file}  = "$key";
  }

  return %ssl;

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

}

# _read_bytes for a length the response announced, which is the whole of the
# difference: it appends onto the accumulator until it has all of $want, and
# an end of stream before then is truncation rather than the end of the body.
#
# Every `last unless $n` in a buffered reader used to be both -- the loop
# ended and what had been collected was returned as the response. Nothing
# compared the two, so a daemon that closed mid-body handed back a short body
# that every return shape this role promises accepts (karr k64).
#
# $into is the same scalar the reader is accumulating into, so the bytes of
# the incomplete piece are in $ctx->{partial} by the time this croaks and go
# out on the exception rather than being dropped.
sub _read_exact {
  my ($self, $sock, $want, $into, $ctx, $phase, $piece) = @_;

  my $read = 0;
  while ($read < $want) {
    my ($n, $buf) = $self->_read_bytes($sock, $want - $read, $ctx);
    last unless $n;
    $$into .= $buf;
    $read += $n;
  }

  $self->_croak_truncated($ctx, phase => $phase, piece => $piece,
    expected => $want, received => $read) if $read < $want;

  return $read;
}

sub _request {
  my ($self, $method, $path, %opts) = @_;

  # Checked while the request is assembled, like a header name: a caller that
  # passes something else gets told before anything reaches the daemon,
  # instead of after the round trip when the metadata fails to arrive.
  croak __PACKAGE__ . '->_request response option must be a HashRef'
    if exists $opts{response} && ref $opts{response} ne 'HASH';

  # Checked here for the same reason, and one at a time: the three units are
  # three shapes the engine's streaming endpoints have, not three views of one
  # stream, so a request asking for two of them has no answer.
  my @streaming = grep { exists $opts{$_} } @STREAM_OPTION;
  croak __PACKAGE__ . '->_request takes one of ' . join(', ', @STREAM_OPTION)
    . ', not ' . join(' and ', @streaming) if @streaming > 1;
  croak __PACKAGE__ . '->_request ' . $streaming[0] . ' option must be a CodeRef'
    if @streaming && ref $opts{$streaming[0]} ne 'CODE';

  my $version = $self->api_version;

  # Caller data -- a container name, an image reference -- is spliced straight
  # into the request line through $path, so it is validated before it can reach
  # the wire, exactly as a header name is. See _assert_request_path.
  $self->_assert_request_path($path);

  my $url_path = defined $version ? "/v$version$path" : $path;

  # Kept before the query string is appended: it names the request in an
  # error message, and the query string is where /build carries buildargs,
  # which can hold credentials and have no business in an exception.
  my $endpoint = $method . ' ' . $url_path;

  # Definedness, not truth. A raw_body of '' (an empty tar) or of the string
  # '0' is a body the caller asked to send, and testing it for truth dropped
  # both: they fell through to the body branch, and the request then went out
  # with no Content-Length, no Content-Type and no payload at all. Whether
  # there is a body is therefore tracked separately from what it says.
  my $body_content;
  my $content_type = 'application/json';
  if (defined $opts{raw_body}) {
    $body_content = $opts{raw_body};
    $content_type = $opts{content_type} // 'application/x-tar';
  }
  elsif ($opts{body}) {
    $body_content = encode_json($opts{body});
  }

  if ($opts{params}) {
    my @pairs;
    for my $k (sort keys %{$opts{params}}) {
      my $v = $opts{params}{$k};
      next unless defined $v;
      # An ArrayRef is one parameter given more than once, not one value:
      # `names => ['a', 'b']` is `names=a&names=b`. That spelling is the only
      # one GET /images/get accepts -- the comma-joined form is read as a
      # single image reference and answered with 500 -- and Go's r.Form[k] is
      # a list for every parameter, so it is the general shape rather than
      # that endpoint's quirk. Element order is the caller's and is kept;
      # only the keys are sorted.
      for my $item (ref $v eq 'ARRAY' ? @$v : ($v)) {
        next unless defined $item;
        push @pairs, _uri_encode($k) . '='
          . _uri_encode(ref $item eq 'HASH' ? encode_json($item) : $item);
      }
    }
    $url_path .= '?' . join('&', @pairs) if @pairs;
  }

  $log->debugf("%s %s", $method, $url_path);

  my $request = "$method $url_path HTTP/1.1\r\n";
  $request .= "Host: localhost\r\n";
  $request .= "Connection: close\r\n";
  $request .= "User-Agent: API-Docker\r\n";

  if (defined $body_content) {
    $request .= "Content-Type: $content_type\r\n";
    $request .= "Content-Length: " . length($body_content) . "\r\n";
  }

  if ($opts{headers}) {
    for my $h (sort keys %{$opts{headers}}) {
      # The name is validated before the value is even looked at: a name that
      # cannot go on the wire is a caller bug whether or not the header ends
      # up being sent.
      $self->_assert_header_name($h);
      my $v = $opts{headers}{$h};
      next unless defined $v;
      $v =~ s/[\r\n]//g;
      $request .= "$h: $v\r\n";

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

    my $docker = API::Docker->new(
      host      => 'tcp://dockerhost:2376',
      tls       => 1,
      cert_path => '/home/me/.docker',
    );

=head3 What the certificates are, and where

C<cert_path> names a directory in the layout the C<docker> CLI writes, and
each of the three files is used if it is there:

=over

=item * F<ca.pem> - the trust anchor the daemon's certificate is checked
against

=item * F<cert.pem> and F<key.pem> - this client's certificate and private
key, sent when the daemon asks the client to identify itself

=back

The two halves of the client certificate go together: one of them present
without the other is a croak, because a key with no certificate proves nothing
and a certificate with no key cannot be used. A directory holding only
F<ca.pem> is fine -- that is a daemon this client verifies but does not
authenticate to. A C<cert_path> that names nothing is a croak: it is read only
once TLS was asked for, and at that point a path pointing nowhere means the
caller believes certificates are in use that are not.

C<cert_path> defaults from C<DOCKER_CERT_PATH>, so on a machine that also runs
the C<docker> CLI it arrives set. Without C<< tls => 1 >> nothing reads it, so
that costs nothing; with it, pass C<< cert_path => undef >> to use the system
trust store instead of the CLI's private one.

=head3 TLS with no certificates at all

It means B<encrypt and verify against the system trust store>, not an error.

C<tls> asks for a connection that is encrypted and whose far end is
authenticated. It does not ask to authenticate this client, which is what the
files on disk are for, and treating the absence of a client certificate as a
missing precondition would conflate the two. The deployment with no
certificate files is real, and is the one this role's documentation used to
recommend before there was any TLS here: a terminator -- nginx, stunnel,
Traefik -- in front of the daemon, holding a publicly trusted certificate.
There is nothing for a C<cert_path> to point at in that setup.

It is also the safe reading rather than the lax one: verification stays on
either way, so the mode reached by configuring nothing is the verifying mode.
A stock C<dockerd --tlsverify> uses a private CA that the system store does
not have, and such a connection fails with a verification error naming exactly
that -- which is the intended outcome, not a silent downgrade. Point
C<cert_path> at the directory holding its F<ca.pem> and it verifies.

=head3 Turning verification off

C<< tls_insecure => 1 >>, and the name is the whole of the warning. It sets
C<SSL_VERIFY_NONE> and switches the hostname check off, which leaves a
connection that is encrypted against a passive listener and against nothing
else: whoever answers chooses the certificate, so anyone able to redirect the
connection reads and rewrites everything on it -- registry credentials,
image contents, the commands containers are started with.

It exists for a self-signed daemon certificate whose CA is genuinely not to
hand. The better answer to that is nearly always F<ca.pem>: a self-signed
certificate is its own CA and can be used as the anchor directly.

=head3 The dependency

L<IO::Socket::SSL> is a B<recommended>, not a required, dependency, and it is
loaded at the moment the first TLS connection is opened. It brings in
L<Net::SSLeay>, which is XS compiled against libssl, and the C<unix://>
transport -- local Docker, rootless Podman, the default -- never needs any of
it; requiring it would make this client unbuildable on a machine with no
OpenSSL headers for the sake of a transport it is not using. Without it,
C<< tls => 1 >> croaks naming the module and how to install it, at the same
point every other connection failure is reported.

=head2 read_timeout

Seconds of silence after which a request gives up and croaks with an
L<API::Docker::Error::Timeout>. C<undef> -- the default, and what every
existing caller gets -- means no timeout at all and is the behaviour this
distribution has always had. C<0> means the same and is the way to say it
explicitly, so a client carrying a default can be opted out of per request.

    my $docker = API::Docker->new(read_timeout => 30);
    $docker->system->using(read_timeout => 0)->events;   # this one may wait

Per request it is an option of L</get>, L</post>, L</put>, L</delete_request>
and L</head>. A resource class carries it through
L<API::Docker::Role::Using/using>, which clones the class rather than taking
it per method -- up for a slow endpoint, down for a stream that should not
stall, off with C<0>.

See L</"Bounding a request that never ends"> for what it does and does not
cover, and L<API::Docker/"What a timeout covers"> for the same question
asked of both bounds at once.

=head2 connect_timeout

Seconds after which opening the connection gives up and croaks with an
L<API::Docker::Error::Timeout> whose C<< ->phase >> is C<'connect'>. C<undef>
-- the default, and what every existing caller gets -- means no bound and is
the behaviour this distribution has always had; C<0> means the same and is the
way to say it explicitly.

    my $docker = API::Docker->new(connect_timeout => 5);
    $docker->system->using(connect_timeout => 0)->version;  # may wait

Separate from L</read_timeout> rather than folded into it, because the two
bound different things and want different numbers: a connect is either
immediate or broken, while a read is waiting on work the daemon has to do.

Per request it is an option of L</get>, L</post>, L</put>, L</delete_request>
and L</head>. A resource class carries it through
L<API::Docker::Role::Using/using>. See L</"Bounding the connection itself">
for what it does on each transport, which is not the same thing on all
three.

=head2 get



( run in 0.677 second using v1.01-cache-2.11-cpan-85d3896f969 )