blob: 00097783f1aceebb2dc4ac96add5d64c4861eb77 [file] [log] [blame] [edit]
// Copyright 2019 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <sysexits.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <optional>
#include <string.h>
#include <curl/curl.h>
#include <brillo/flag_helper.h>
#include <chromeos/libipp/attribute.h>
#include <chromeos/libipp/builder.h>
#include <chromeos/libipp/frame.h>
#include <chromeos/libipp/parser.h>
#include "helpers.h"
#include "ipp_in_json.h"
namespace {
// Help message about the application.
constexpr char app_info[] =
"This tool tries to send IPP "
"Get-Printer-Attributes request to given URL and parse obtained "
"response. If no output files are specified, the obtained response "
"is printed to stdout as formatted JSON";
struct read_cb_data {
const std::vector<uint8_t>* input_data;
size_t read_position;
};
// Read callback for libcurl; reads from a vector in memory.
size_t read_callback(char* buffer, size_t size, size_t nitems, void* userdata) {
size_t read_size = size * nitems;
read_cb_data* read_userdata = static_cast<read_cb_data*>(userdata);
const std::vector<uint8_t>* input_data = read_userdata->input_data;
size_t input_start_pos = read_userdata->read_position;
size_t bytes_available = input_data->size() - input_start_pos;
if (bytes_available < read_size) {
read_size = bytes_available;
}
if (read_size > 0) {
memcpy(buffer, input_data->data() + input_start_pos, read_size);
read_userdata->read_position += read_size;
}
return read_size;
}
// Write callback for libcurl; writes to a vector in memory.
size_t write_callback(char* buffer,
size_t size,
size_t nitems,
void* userdata) {
size_t write_size = size * nitems;
if (!write_size) {
return 0;
}
std::vector<uint8_t>* output_data =
static_cast<std::vector<uint8_t>*>(userdata);
const uint8_t* data_to_copy = (const uint8_t*)buffer;
output_data->insert(output_data->end(), data_to_copy,
data_to_copy + write_size);
return write_size;
}
// Sends IPP frame (in |data| parameter) to given URL. In case of error, it
// prints out error message to stderr and returns nullopt. Otherwise, it returns
// the body from the response.
std::optional<std::vector<uint8_t>> SendIppFrameAndGetResponse(
std::string url, const std::vector<uint8_t>& input_data) {
CURL* curl;
CURLcode curl_result;
std::vector<uint8_t> output_data;
std::optional<std::vector<uint8_t>> return_value = std::nullopt;
read_cb_data read_userdata = {&input_data, 0};
curl_result = curl_global_init(CURL_GLOBAL_DEFAULT);
if (curl_result != CURLE_OK) {
std::cerr << "Error: failed to initialize curl\n";
return std::nullopt;
}
curl = curl_easy_init();
if (!curl) {
std::cerr << "Error: failed to initialize curl\n";
curl_global_cleanup();
return std::nullopt;
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_POST, 1L);
// Add Content-Type header to request.
curl_slist* header_list =
curl_slist_append(nullptr, "Content-Type: application/ipp");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
// Printers usually have self-signed certificates that won't be accepted by
// any certificate database on the system. Since printer_diag is only a
// debugging tool for gathering information about a printer, we don't need
// or want to be strict about it. This unique need is why this function uses
// libcurl directly instead of going through brillo's HTTP library.
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
// Follow redirects.
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
// Set our callbacks to read from and write to std::vector<uint8_t>
curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);
curl_easy_setopt(curl, CURLOPT_READDATA, static_cast<void*>(&read_userdata));
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, static_cast<void*>(&output_data));
// Actually do the request.
curl_result = curl_easy_perform(curl);
if (curl_result != CURLE_OK) {
std::cerr << "HTTP error: " << curl_easy_strerror(curl_result) << "\n";
} else {
auto response_code = 999;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
// Per RFC 8010 section 3.4.3, any HTTP status code other than 200 means
// the response does not contain an IPP message body.
if (response_code != 200) {
std::cerr << "HTTP error: HTTP response code " << response_code << "\n";
} else {
return_value = std::move(output_data);
}
}
curl_slist_free_all(header_list);
curl_easy_cleanup(curl);
curl_global_cleanup();
return return_value;
}
// Write the content of given buffer to given filename ("location"). When
// "location" equals "-", the content is written to stdout. In case of an error,
// it prints out error message to stderr and returns false. The "buffer"
// parameter cannot be nullptr, exactly "size" elements is read from it.
bool WriteBufferToLocation(const char* buffer,
unsigned size,
const std::string& location) {
if (location == "-") {
std::cout.write(buffer, size);
std::cout << std::endl;
if (std::cout.bad()) {
std::cerr << "Error when writing results to standard output.\n";
return false;
}
} else {
std::ofstream file(location, std::ios::binary | std::ios::trunc);
if (!file.good()) {
std::cerr << "Error when opening the file " << location << ".\n";
return false;
}
file.write(buffer, size);
file.close();
if (file.bad()) {
std::cerr << "Error when writing to the file " << location << ".\n";
return false;
}
}
return true;
}
} // namespace
// Return codes:
// * EX_USAGE or EX_DATAERR: incorrect command line parameters
// * -1: cannot build IPP request (libipp error)
// * -2: HTTP exchange error (brillo/http or HTTP error)
// * -3: cannot save an output to given file (I/O error?)
// * -4: cannot build JSON output (base/json error).
// * -5: cannot parse IPP response (incorrect frame was received)
int main(int argc, char** argv) {
// Define and parse command line parameters, exit if incorrect.
DEFINE_string(
url, "", "Address to query, supported protocols: http, https, ipp, ipps");
DEFINE_string(version, "1.1", "IPP version (default 1.1)");
DEFINE_string(
jsonf, "",
"Save the response as formatted JSON to given file (use - for stdout)");
DEFINE_string(
jsonc, "",
"Save the response as compressed JSON to given file (use - for stdout)");
DEFINE_string(
binary, "",
"Dump the response to given file as a binary content (use - for stdout)");
brillo::FlagHelper::Init(argc, argv, app_info);
auto free_params = base::CommandLine::ForCurrentProcess()->GetArgs();
if (!free_params.empty()) {
std::cerr << "Unknown parameters:";
for (auto param : free_params) {
std::cerr << " " << param;
}
std::cerr << std::endl;
return EX_USAGE;
}
// Replace ipp/ipps protocol in the given URL to http/https (if needed).
if (!ConvertIppToHttp(FLAGS_url)) {
return EX_USAGE;
}
std::cerr << "URL: " << FLAGS_url << std::endl;
// Parse the IPP version.
ipp::Version version;
if (!ipp::FromString(FLAGS_version, &version)) {
std::cerr << "Unknown version: " << FLAGS_version << ". ";
std::cerr << "Allowed values: 1.0, 1.1, 2.0, 2.1, 2.2." << std::endl;
return EX_USAGE;
}
std::cerr << "IPP version: " << ipp::ToString(version) << std::endl;
// If no output files were specified, set the default settings.
if (FLAGS_binary.empty() && FLAGS_jsonc.empty() && FLAGS_jsonf.empty())
FLAGS_jsonf = "-";
// Send IPP request and get a response.
ipp::Frame request(ipp::Operation::Get_Printer_Attributes, version);
ipp::Collection& grp = request.Groups(ipp::GroupTag::operation_attributes)[0];
grp.AddAttr("printer-uri", ipp::ValueTag::uri, FLAGS_url);
grp.AddAttr("requested-attributes", ipp::ValueTag::keyword,
std::vector<std::string>{"all", "media-col-database"});
std::vector<uint8_t> data = ipp::BuildBinaryFrame(request);
// Resolve the IP after setting printer-uri so the printer can see the
// original name.
if (!ResolveZeroconfHostname(FLAGS_url)) {
return EX_DATAERR;
}
auto data_optional = SendIppFrameAndGetResponse(FLAGS_url, data);
if (!data_optional)
return -2;
data = std::move(*data_optional);
// Write raw frame to file if needed.
if (!FLAGS_binary.empty()) {
if (!WriteBufferToLocation(reinterpret_cast<const char*>(data.data()),
data.size(), FLAGS_binary)) {
return -3;
}
}
// Parse the IPP response and save results.
int return_code = 0;
ipp::SimpleParserLog log;
ipp::Frame response = ipp::Parse(data.data(), data.size(), log);
if (!log.CriticalErrors().empty()) {
std::cerr << "Parsing of an obtained response was not completed."
<< std::endl;
return_code = -5;
// Let's continue, we can still return some data (it is not our error).
}
if (!FLAGS_jsonc.empty()) {
std::string json;
if (!ConvertToJson(response, log, true, &json)) {
std::cerr << "Error when preparing a report in JSON (compressed)."
<< std::endl;
return -4;
}
if (!WriteBufferToLocation(json.data(), json.size(), FLAGS_jsonc)) {
return -3;
}
}
if (!FLAGS_jsonf.empty()) {
std::string json;
if (!ConvertToJson(response, log, false, &json)) {
std::cerr << "Error when preparing a report in JSON (formatted)."
<< std::endl;
return -4;
}
if (!WriteBufferToLocation(json.data(), json.size(), FLAGS_jsonf)) {
return -3;
}
}
return return_code;
}