| // Copyright 2020 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "escl_manager.h" |
| |
| #include <optional> |
| #include <utility> |
| |
| #include <base/containers/contains.h> |
| #include <base/files/file_util.h> |
| #include <base/json/json_writer.h> |
| #include <base/strings/stringprintf.h> |
| #include <base/logging.h> |
| #include <base/strings/string_split.h> |
| #include <base/strings/string_util.h> |
| #include <base/threading/platform_thread.h> |
| #include <crypto/random.h> |
| |
| #include "jpeg_util.h" |
| #include "xml_util.h" |
| |
| namespace { |
| |
| std::optional<std::vector<std::string>> ExtractStringList( |
| const base::Value& root, const std::string& config_name) { |
| const base::Value* value = |
| root.FindKeyOfType(config_name, base::Value::Type::LIST); |
| if (!value) { |
| LOG(ERROR) << "Config is missing " << config_name << " settings"; |
| return std::nullopt; |
| } |
| std::vector<std::string> config_list; |
| for (const base::Value& v : value->GetList()) { |
| if (!v.is_string()) { |
| LOG(ERROR) << config_name << " value expected string, not " << v.type(); |
| return std::nullopt; |
| } |
| config_list.push_back(v.GetString()); |
| } |
| return config_list; |
| } |
| |
| std::optional<std::vector<int>> ExtractIntList(const base::Value& root, |
| const std::string& config_name) { |
| const base::Value* value = |
| root.FindKeyOfType(config_name, base::Value::Type::LIST); |
| if (!value) { |
| LOG(ERROR) << "Config is missing " << config_name << " settings"; |
| return std::nullopt; |
| } |
| std::vector<int> config_list; |
| for (const base::Value& v : value->GetList()) { |
| if (!v.is_int()) { |
| LOG(ERROR) << config_name << " value expected int, not " << v.type(); |
| return std::nullopt; |
| } |
| config_list.push_back(v.GetInt()); |
| } |
| return config_list; |
| } |
| |
| std::optional<std::string> ExtractString(const base::Value& root, |
| const std::string& config_name) { |
| const base::Value* value = |
| root.FindKeyOfType(config_name, base::Value::Type::STRING); |
| if (!value) { |
| LOG(ERROR) << "Config is missing " << config_name << " setting"; |
| return std::nullopt; |
| } |
| return value->GetString(); |
| } |
| |
| std::optional<int> ExtractInt(const base::Value& root, |
| const std::string& config_name) { |
| const base::Value* value = |
| root.FindKeyOfType(config_name, base::Value::Type::INTEGER); |
| if (!value) { |
| return std::nullopt; |
| } |
| return value->GetInt(); |
| } |
| |
| std::optional<SourceCapabilities> CreateSourceCapabilitiesFromConfig( |
| const base::Value& config) { |
| if (!config.is_dict()) { |
| LOG(ERROR) << "Cannot initialize SourceCapabilities from non-dict value"; |
| return std::nullopt; |
| } |
| |
| SourceCapabilities result; |
| result.max_width = ExtractInt(config, "MaxWidth"); |
| result.max_height = ExtractInt(config, "MaxHeight"); |
| |
| std::optional<std::vector<std::string>> color_modes = |
| ExtractStringList(config, "ColorModes"); |
| if (!color_modes) { |
| LOG(ERROR) << "Could not find valid ColorModes config"; |
| return std::nullopt; |
| } |
| result.color_modes = color_modes.value(); |
| |
| std::optional<std::vector<std::string>> formats = |
| ExtractStringList(config, "DocumentFormats"); |
| if (!formats) { |
| LOG(ERROR) << "Could not find valid DocumentFormats config"; |
| return std::nullopt; |
| } |
| result.formats = formats.value(); |
| |
| std::optional<std::vector<int>> resolutions = |
| ExtractIntList(config, "Resolutions"); |
| if (!resolutions) { |
| LOG(ERROR) << "Could not find valid Resolutions config"; |
| return std::nullopt; |
| } |
| result.resolutions = resolutions.value(); |
| |
| result.x_justification = ExtractString(config, "XJustification"); |
| |
| return result; |
| } |
| |
| // Generates a hyphenated UUID v4 (random). |
| // An example UUID looks like "0b2cdf31-edee-4246-a1ad-07bbe754856b" |
| std::string GenerateUUID() { |
| // We only need 16 bytes of randomness, not 32, but using 32 makes the rest of |
| // the code a little clearer. |
| std::vector<uint8_t> bytes(32); |
| crypto::RandBytes(bytes.data(), bytes.size()); |
| |
| std::string uuid; |
| for (int i = 0; i < 32; i++) { |
| if (i == 8 || i == 12 || i == 16 || i == 20) |
| uuid += "-"; |
| |
| uint8_t val = bytes[i] % 16; |
| if (i == 12) { |
| val = 4; |
| } else if (i == 16) { |
| val &= ~0x4; |
| val |= 0x8; |
| } |
| |
| if (0 <= val && val <= 9) |
| uuid += '0' + val; |
| else |
| uuid += 'a' + (val - 10); |
| } |
| return uuid; |
| } |
| |
| std::string JobStateAsString(JobState state) { |
| switch (state) { |
| case kCanceled: |
| return "Canceled"; |
| case kCompleted: |
| return "Completed"; |
| case kPending: |
| return "Pending"; |
| } |
| } |
| |
| std::string ColorModeAsString(ColorMode mode) { |
| switch (mode) { |
| case kBlackAndWhite: |
| return "BlackAndWhite1"; |
| case kGrayscale: |
| return "Grayscale8"; |
| case kRGB: |
| return "RGB24"; |
| } |
| } |
| |
| uint8_t GetTotalPagesForJob(const ScanSettings& settings) { |
| // Two scan jobs consist of multiple pages: |
| // * ADF grayscale 300 dpi. |
| // * ADF color 100 dpi. |
| if ((settings.input_source == "Feeder" && settings.color_mode == kGrayscale && |
| settings.x_resolution == 300 && settings.y_resolution == 300) || |
| (settings.input_source == "Feeder" && settings.color_mode == kRGB && |
| settings.x_resolution == 100 && settings.y_resolution == 100)) { |
| return 2; |
| } |
| |
| return 1; |
| } |
| |
| std::optional<base::TimeDelta> GetDelayForJob(const ScanSettings& settings) { |
| // The only scan job which sends a delayed response is a Platen scan which is |
| // color and 100 dpi. |
| if (settings.input_source == "Platen" && settings.color_mode == kRGB && |
| settings.x_resolution == 100 && settings.y_resolution == 100) { |
| return base::Seconds(45); |
| } |
| |
| return std::nullopt; |
| } |
| |
| } // namespace |
| |
| std::optional<ScannerCapabilities> CreateScannerCapabilitiesFromConfig( |
| const base::Value& config) { |
| if (!config.is_dict()) { |
| LOG(ERROR) << "Cannot initialize ScannerCapabilities from non-dict value"; |
| return std::nullopt; |
| } |
| |
| ScannerCapabilities result; |
| const base::Value* value = nullptr; |
| value = config.FindKeyOfType("MakeAndModel", base::Value::Type::STRING); |
| if (!value) { |
| LOG(ERROR) << "Config is missing MakeAndModel setting"; |
| return std::nullopt; |
| } |
| result.make_and_model = value->GetString(); |
| |
| value = config.FindKeyOfType("SerialNumber", base::Value::Type::STRING); |
| if (!value) { |
| LOG(ERROR) << "Config is missing SerialNumber setting"; |
| return std::nullopt; |
| } |
| result.serial_number = value->GetString(); |
| |
| value = config.FindKeyOfType("Platen", base::Value::Type::DICTIONARY); |
| if (!value) { |
| LOG(ERROR) << "Config is missing Platen source capabilities"; |
| return std::nullopt; |
| } |
| |
| std::optional<SourceCapabilities> platen_capabilities = |
| CreateSourceCapabilitiesFromConfig(*value); |
| if (!platen_capabilities) { |
| LOG(ERROR) << "Parsing Platen capabilities failed"; |
| return std::nullopt; |
| } |
| result.platen_capabilities = platen_capabilities.value(); |
| |
| value = config.FindKeyOfType("ADF", base::Value::Type::DICTIONARY); |
| if (value) { |
| std::optional<SourceCapabilities> adf_capabilities = |
| CreateSourceCapabilitiesFromConfig(*value); |
| if (!adf_capabilities) { |
| LOG(ERROR) << "Parsing ADF capabilities failed"; |
| return std::nullopt; |
| } |
| result.adf_capabilities = adf_capabilities; |
| } |
| |
| return result; |
| } |
| |
| EsclManager::EsclManager(ScannerCapabilities scanner_capabilities, |
| const base::FilePath& output_log_dir) |
| : scanner_capabilities_(std::move(scanner_capabilities)), |
| output_log_dir_(output_log_dir), |
| log_counter_(1) { |
| status_.idle = true; |
| status_.adf_empty = false; |
| } |
| |
| HttpResponse EsclManager::HandleEsclRequest(const HttpRequest& request, |
| const SmartBuffer& request_body) { |
| if (request.method == "GET" && request.uri == "/eSCL/ScannerCapabilities") { |
| HttpResponse response; |
| response.status = "200 OK"; |
| response.headers["Content-Type"] = "text/xml"; |
| response.body.Add(ScannerCapabilitiesAsXml(scanner_capabilities_)); |
| return response; |
| } else if (request.method == "GET" && request.uri == "/eSCL/ScannerStatus") { |
| HttpResponse response; |
| response.status = "200 OK"; |
| response.headers["Content-Type"] = "text/xml"; |
| response.body.Add(ScannerStatusAsXml(status_)); |
| status_.adf_empty = false; |
| return response; |
| } else if (request.method == "POST" && request.uri == "/eSCL/ScanJobs") { |
| return HandleCreateScanJob(request_body); |
| } else if (request.method == "GET" && |
| base::StartsWith(request.uri, "/eSCL/ScanJobs/", |
| base::CompareCase::SENSITIVE)) { |
| return HandleGetNextDocument(request.uri); |
| } else if (request.method == "DELETE" && |
| base::StartsWith(request.uri, "/eSCL/ScanJobs/", |
| base::CompareCase::SENSITIVE)) { |
| return HandleDeleteJob(request.uri); |
| } else if (request.uri == "/eSCL/ScannerCapabilities" || |
| request.uri == "/eSCL/ScannerStatus" || |
| base::StartsWith(request.uri, "/eSCL/ScanJobs", |
| base::CompareCase::SENSITIVE)) { |
| LOG(ERROR) << "Unexpected request method " << request.method |
| << " for endpoint " << request.uri; |
| HttpResponse response; |
| response.status = "405 Method Not Allowed"; |
| return response; |
| } else { |
| LOG(ERROR) << "Unknown eSCL endpoint " << request.uri << " (method is " |
| << request.method << ")"; |
| HttpResponse response; |
| response.status = "404 Not Found"; |
| return response; |
| } |
| } |
| |
| // Generates an HTTP response for a POST request to the /eSCL/ScanJobs endpoint. |
| HttpResponse EsclManager::HandleCreateScanJob(const SmartBuffer& request_body) { |
| HttpResponse response; |
| |
| std::optional<ScanSettings> settings_opt = |
| ScanSettingsFromXml(request_body.contents()); |
| if (!settings_opt) { |
| LOG(ERROR) << "Could not parse ScanSettings from request body"; |
| response.status = "415 Unsupported Media Type"; |
| return response; |
| } |
| ScanSettings settings = settings_opt.value(); |
| if (!LogSettings(settings)) { |
| LOG(ERROR) << "Failed to log settings."; |
| } |
| |
| SourceCapabilities caps; |
| if (settings.input_source == "Platen") { |
| caps = scanner_capabilities_.platen_capabilities; |
| } else if (settings.input_source == "Feeder") { |
| if (!scanner_capabilities_.adf_capabilities.has_value()) { |
| LOG(ERROR) << "Requested ADF source but printer doesn't have an ADF"; |
| response.status = "409 Conflict"; |
| return response; |
| } |
| caps = scanner_capabilities_.adf_capabilities.value(); |
| } else { |
| LOG(ERROR) << "Requested unknown source '" << settings.input_source << "'"; |
| response.status = "409 Conflict"; |
| return response; |
| } |
| |
| if (!base::Contains(caps.color_modes, |
| ColorModeAsString(settings.color_mode))) { |
| LOG(ERROR) << "Requested unsupported color mode '" |
| << ColorModeAsString(settings.color_mode) << "'"; |
| for (const auto& mode : caps.color_modes) { |
| LOG(ERROR) << "modes: " << mode; |
| } |
| response.status = "409 Conflict"; |
| return response; |
| } |
| |
| if (!base::Contains(caps.formats, settings.document_format)) { |
| LOG(ERROR) << "Requested unsupported document format '" |
| << settings.document_format << "'"; |
| response.status = "409 Conflict"; |
| return response; |
| } |
| |
| if (settings.x_resolution != settings.y_resolution) { |
| LOG(ERROR) << "Scanner cannot support different resolutions in X and Y: " |
| << settings.x_resolution << " " << settings.y_resolution; |
| response.status = "409 Conflict"; |
| return response; |
| } |
| |
| if (!base::Contains(caps.resolutions, settings.x_resolution)) { |
| LOG(ERROR) << "Requested unsupported resolution '" << settings.x_resolution |
| << "'"; |
| response.status = "409 Conflict"; |
| return response; |
| } |
| |
| if (settings.input_source == "Feeder" && settings.color_mode == kGrayscale && |
| settings.x_resolution == 600 && settings.y_resolution == 600) { |
| status_.adf_empty = true; |
| response.status = "500 Internal Server Error"; |
| return response; |
| } |
| |
| std::string uuid = GenerateUUID(); |
| JobInfo job; |
| job.created = base::TimeTicks::Now(); |
| job.state = kPending; |
| job.settings = std::move(settings); |
| job.num_remaining_pages = GetTotalPagesForJob(job.settings); |
| job.delay = GetDelayForJob(job.settings); |
| status_.jobs[uuid] = job; |
| |
| response.status = "201 Created"; |
| response.headers["Location"] = "/eSCL/ScanJobs/" + uuid; |
| response.headers["Pragma"] = "no-cache"; |
| return response; |
| } |
| |
| // Generates an HTTP response containing scan data for a previously created |
| // scan job. |
| // The URI should be formatted as: |
| // "/eSCL/ScanJobs/0b2cdf31-edee-4246-a1ad-07bbe754856b/NextDocument" |
| HttpResponse EsclManager::HandleGetNextDocument(const std::string& uri) { |
| HttpResponse response; |
| std::vector<std::string> tokens = |
| base::SplitString(uri, "/", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| if (tokens.size() != 5 || tokens[4] != "NextDocument") { |
| LOG(ERROR) << "Malformed GET ScanJobs request URI: " << uri; |
| response.status = "405 Method Not Allowed"; |
| return response; |
| } |
| |
| std::string uuid = tokens[3]; |
| if (status_.jobs.count(uuid) == 0) { |
| LOG(ERROR) << "No job found with uuid: " << uuid; |
| response.status = "404 Not Found"; |
| return response; |
| } |
| |
| JobInfo info = status_.jobs[uuid]; |
| switch (info.state) { |
| case kCanceled: |
| case kCompleted: |
| LOG(INFO) << "Not providing NextDocument for " |
| << JobStateAsString(info.state) << " job."; |
| response.status = "404 Not Found"; |
| break; |
| case kPending: { |
| status_.jobs[uuid].num_remaining_pages--; |
| if (status_.jobs[uuid].num_remaining_pages == 0) |
| status_.jobs[uuid].state = kCompleted; |
| std::optional<std::vector<uint8_t>> image = |
| GenerateJpegFromScanSettings(info.settings); |
| if (!image.has_value()) { |
| LOG(ERROR) << "Failed to generate image, sending error response."; |
| response.status = "500 Internal Server Error"; |
| } else { |
| response.status = "200 OK"; |
| response.headers["Content-Type"] = "image/jpeg"; |
| response.body.Add(image.value()); |
| |
| // Unfortunately we don't have access to a callback to post a delayed |
| // task which runs the callback, so the easiest way to delay the |
| // response is just to sleep. |
| if (info.delay.has_value()) |
| base::PlatformThread::Sleep(info.delay.value()); |
| } |
| break; |
| } |
| } |
| |
| return response; |
| } |
| |
| // Generates an HTTP response to a request to delete the ScanJob at |uri|. |
| HttpResponse EsclManager::HandleDeleteJob(const std::string& uri) { |
| HttpResponse response; |
| std::vector<std::string> tokens = |
| base::SplitString(uri, "/", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| if (tokens.size() != 4) { |
| LOG(ERROR) << "Malformed DELETE ScanJobs request URI: " << uri; |
| response.status = "405 Method Not Allowed"; |
| return response; |
| } |
| |
| std::string uuid = tokens[3]; |
| size_t erased = status_.jobs.erase(uuid); |
| if (erased == 1) { |
| response.status = "200 OK"; |
| } else { |
| response.status = "404 Not Found"; |
| } |
| return response; |
| } |
| |
| bool EsclManager::LogSettings(const ScanSettings& settings) { |
| if (output_log_dir_.empty()) |
| return true; |
| |
| base::Value settings_dict = SettingsToDict(settings); |
| std::string json_settings; |
| if (!base::JSONWriter::Write(settings_dict, &json_settings)) { |
| LOG(ERROR) << "Failed to write settings in json format."; |
| return false; |
| } |
| |
| base::FilePath json_file_path = output_log_dir_.Append( |
| base::StringPrintf("%02d_createscanjob.json", log_counter_)); |
| if (!base::WriteFile(json_file_path, json_settings)) { |
| LOG(ERROR) << "Failed to write settings to json file."; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| base::Value EsclManager::SettingsToDict(const ScanSettings& settings) { |
| base::flat_map<std::string, base::Value> dict; |
| dict["DocumentFormat"] = base::Value(settings.document_format); |
| dict["ColorMode"] = base::Value(settings.color_mode); |
| dict["InputSource"] = base::Value(settings.input_source); |
| dict["XResolution"] = base::Value(settings.x_resolution); |
| dict["YResolution"] = base::Value(settings.y_resolution); |
| std::vector<base::Value> regions; |
| for (const ScanRegion& r : settings.regions) { |
| base::flat_map<std::string, base::Value> region_dict; |
| region_dict["Units"] = base::Value(r.units); |
| region_dict["Height"] = base::Value(r.height); |
| region_dict["Width"] = base::Value(r.width); |
| region_dict["XOffset"] = base::Value(r.x_offset); |
| region_dict["YOffset"] = base::Value(r.y_offset); |
| regions.push_back(base::Value(region_dict)); |
| } |
| dict["Regions"] = base::Value(regions); |
| |
| return base::Value(dict); |
| } |