pro.cpp 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/FileStream.h>
  7. #include <AK/GenericLexer.h>
  8. #include <AK/LexicalPath.h>
  9. #include <AK/NumberFormat.h>
  10. #include <AK/URL.h>
  11. #include <LibCore/ArgsParser.h>
  12. #include <LibCore/EventLoop.h>
  13. #include <LibCore/File.h>
  14. #include <LibProtocol/Request.h>
  15. #include <LibProtocol/RequestClient.h>
  16. #include <ctype.h>
  17. #include <stdio.h>
  18. // FIXME: Move this somewhere else when it's needed (e.g. in the Browser)
  19. class ContentDispositionParser {
  20. public:
  21. ContentDispositionParser(const StringView& value)
  22. {
  23. GenericLexer lexer(value);
  24. lexer.ignore_while(isspace);
  25. if (lexer.consume_specific("inline")) {
  26. m_kind = Kind::Inline;
  27. if (!lexer.is_eof())
  28. m_might_be_wrong = true;
  29. return;
  30. }
  31. if (lexer.consume_specific("attachment")) {
  32. m_kind = Kind::Attachment;
  33. if (lexer.consume_specific(";")) {
  34. lexer.ignore_while(isspace);
  35. if (lexer.consume_specific("filename=")) {
  36. // RFC 2183: "A short (length <= 78 characters)
  37. // parameter value containing only non-`tspecials' characters SHOULD be
  38. // represented as a single `token'."
  39. // Some people seem to take this as generic advice of "if it doesn't have special characters,
  40. // it's safe to specify as a single token"
  41. // So let's just be as lenient as possible.
  42. if (lexer.next_is('"'))
  43. m_filename = lexer.consume_quoted_string();
  44. else
  45. m_filename = lexer.consume_until(is_any_of("()<>@,;:\\\"/[]?= "));
  46. } else {
  47. m_might_be_wrong = true;
  48. }
  49. }
  50. return;
  51. }
  52. if (lexer.consume_specific("form-data")) {
  53. m_kind = Kind::FormData;
  54. while (lexer.consume_specific(";")) {
  55. lexer.ignore_while(isspace);
  56. if (lexer.consume_specific("name=")) {
  57. m_name = lexer.consume_quoted_string();
  58. } else if (lexer.consume_specific("filename=")) {
  59. if (lexer.next_is('"'))
  60. m_filename = lexer.consume_quoted_string();
  61. else
  62. m_filename = lexer.consume_until(is_any_of("()<>@,;:\\\"/[]?= "));
  63. } else {
  64. m_might_be_wrong = true;
  65. }
  66. }
  67. return;
  68. }
  69. // FIXME: Support 'filename*'
  70. m_might_be_wrong = true;
  71. }
  72. enum class Kind {
  73. Inline,
  74. Attachment,
  75. FormData,
  76. };
  77. const StringView& filename() const { return m_filename; }
  78. const StringView& name() const { return m_name; }
  79. Kind kind() const { return m_kind; }
  80. bool might_be_wrong() const { return m_might_be_wrong; }
  81. private:
  82. StringView m_filename;
  83. StringView m_name;
  84. Kind m_kind { Kind::Inline };
  85. bool m_might_be_wrong { false };
  86. };
  87. template<typename ConditionT>
  88. class ConditionalOutputFileStream final : public OutputFileStream {
  89. public:
  90. template<typename... Args>
  91. ConditionalOutputFileStream(ConditionT&& condition, Args... args)
  92. : OutputFileStream(args...)
  93. , m_condition(condition)
  94. {
  95. }
  96. ~ConditionalOutputFileStream()
  97. {
  98. if (!m_condition())
  99. return;
  100. if (!m_buffer.is_empty()) {
  101. OutputFileStream::write(m_buffer);
  102. m_buffer.clear();
  103. }
  104. }
  105. private:
  106. size_t write(ReadonlyBytes bytes) override
  107. {
  108. if (!m_condition()) {
  109. write_to_buffer:;
  110. m_buffer.append(bytes.data(), bytes.size());
  111. return bytes.size();
  112. }
  113. if (!m_buffer.is_empty()) {
  114. auto size = OutputFileStream::write(m_buffer);
  115. m_buffer = m_buffer.slice(size, m_buffer.size() - size);
  116. }
  117. if (!m_buffer.is_empty())
  118. goto write_to_buffer;
  119. return OutputFileStream::write(bytes);
  120. }
  121. ConditionT m_condition;
  122. ByteBuffer m_buffer;
  123. };
  124. int main(int argc, char** argv)
  125. {
  126. const char* url_str = nullptr;
  127. bool save_at_provided_name = false;
  128. const char* data = nullptr;
  129. String method = "GET";
  130. HashMap<String, String, CaseInsensitiveStringTraits> request_headers;
  131. Core::ArgsParser args_parser;
  132. args_parser.set_general_help(
  133. "Request a file from an arbitrary URL. This command uses RequestServer, "
  134. "and thus supports at least http, https, and gemini.");
  135. args_parser.add_option(save_at_provided_name, "Write to a file named as the remote file", nullptr, 'O');
  136. args_parser.add_option(data, "(HTTP only) Send the provided data via an HTTP POST request", "data", 'd', "data");
  137. args_parser.add_option(Core::ArgsParser::Option {
  138. .requires_argument = true,
  139. .help_string = "Add a header entry to the request",
  140. .long_name = "header",
  141. .short_name = 'H',
  142. .value_name = "header-value",
  143. .accept_value = [&](auto* s) {
  144. StringView header { s };
  145. auto split = header.find_first_of(':');
  146. if (!split.has_value())
  147. return false;
  148. request_headers.set(header.substring_view(0, split.value()), header.substring_view(split.value() + 1));
  149. return true;
  150. } });
  151. args_parser.add_positional_argument(url_str, "URL to download from", "url");
  152. args_parser.parse(argc, argv);
  153. if (data) {
  154. method = "POST";
  155. // FIXME: Content-Type?
  156. }
  157. URL url(url_str);
  158. if (!url.is_valid()) {
  159. warnln("'{}' is not a valid URL", url_str);
  160. return 1;
  161. }
  162. Core::EventLoop loop;
  163. auto protocol_client = Protocol::RequestClient::construct();
  164. auto request = protocol_client->start_request(method, url, request_headers, data ? StringView { data }.bytes() : ReadonlyBytes {});
  165. if (!request) {
  166. warnln("Failed to start request for '{}'", url_str);
  167. return 1;
  168. }
  169. u32 previous_downloaded_size { 0 };
  170. u32 previous_midpoint_downloaded_size { 0 };
  171. timeval prev_time, prev_midpoint_time, current_time, time_diff;
  172. static constexpr auto download_speed_rolling_average_time_in_ms = 4000;
  173. gettimeofday(&prev_time, nullptr);
  174. bool received_actual_headers = false;
  175. request->on_progress = [&](Optional<u32> maybe_total_size, u32 downloaded_size) {
  176. warn("\r\033[2K");
  177. if (maybe_total_size.has_value()) {
  178. warn("\033]9;{};{};\033\\", downloaded_size, maybe_total_size.value());
  179. warn("Download progress: {} / {}", human_readable_size(downloaded_size), human_readable_size(maybe_total_size.value()));
  180. } else {
  181. warn("Download progress: {} / ???", human_readable_size(downloaded_size));
  182. }
  183. gettimeofday(&current_time, nullptr);
  184. timersub(&current_time, &prev_time, &time_diff);
  185. auto time_diff_ms = time_diff.tv_sec * 1000 + time_diff.tv_usec / 1000;
  186. auto size_diff = downloaded_size - previous_downloaded_size;
  187. warn(" at {}/s", human_readable_size(((float)size_diff / (float)time_diff_ms) * 1000));
  188. if (time_diff_ms >= download_speed_rolling_average_time_in_ms) {
  189. previous_downloaded_size = previous_midpoint_downloaded_size;
  190. prev_time = prev_midpoint_time;
  191. } else if (time_diff_ms >= download_speed_rolling_average_time_in_ms / 2) {
  192. previous_midpoint_downloaded_size = downloaded_size;
  193. prev_midpoint_time = current_time;
  194. }
  195. };
  196. if (save_at_provided_name) {
  197. request->on_headers_received = [&](auto& response_headers, auto status_code) {
  198. if (received_actual_headers)
  199. return;
  200. dbgln("Received headers! response code = {}", status_code.value_or(0));
  201. received_actual_headers = true; // And not trailers!
  202. String output_name;
  203. if (auto content_disposition = response_headers.get("Content-Disposition"); content_disposition.has_value()) {
  204. auto& value = content_disposition.value();
  205. ContentDispositionParser parser(value);
  206. output_name = parser.filename();
  207. }
  208. if (output_name.is_empty())
  209. output_name = url.path();
  210. LexicalPath path { output_name };
  211. output_name = path.basename();
  212. // The URL didn't have a name component, e.g. 'serenityos.org'
  213. if (output_name.is_empty() || output_name == "/") {
  214. int i = -1;
  215. do {
  216. output_name = url.host();
  217. if (i > -1)
  218. output_name = String::formatted("{}.{}", output_name, i);
  219. ++i;
  220. } while (Core::File::exists(output_name));
  221. }
  222. if (freopen(output_name.characters(), "w", stdout) == nullptr) {
  223. perror("freopen");
  224. loop.quit(1);
  225. return;
  226. }
  227. };
  228. }
  229. request->on_finish = [&](bool success, auto) {
  230. warn("\033]9;-1;\033\\");
  231. warnln();
  232. if (!success)
  233. warnln("Request failed :(");
  234. loop.quit(0);
  235. };
  236. auto output_stream = ConditionalOutputFileStream { [&] { return save_at_provided_name ? received_actual_headers : true; }, stdout };
  237. request->stream_into(output_stream);
  238. dbgln("started request with id {}", request->id());
  239. auto rc = loop.exec();
  240. // FIXME: This shouldn't be needed.
  241. fclose(stdout);
  242. return rc;
  243. }