/** * mega_http_client_get_response_length: * @http_client: a #MegaHttpClient * @cancellable: * @err * * Description... * * Returns: */ gint64 mega_http_client_get_response_length(MegaHttpClient* http_client, GCancellable* cancellable, GError** err) { GError* local_err = NULL; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), -1); MegaHttpClientPrivate* priv = http_client->priv; g_rec_mutex_lock(priv->lock); if (!goto_state(http_client, CONN_STATE_HEADERS_RECEIVED, cancellable, &local_err)) { g_rec_mutex_unlock(priv->lock); g_propagate_error(err, local_err); return -1; } if (priv->response_length < 0) { g_rec_mutex_unlock(priv->lock); g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Response length not set"); return -1; } gint response_length = priv->response_length; g_rec_mutex_unlock(priv->lock); return response_length; }
/** * mega_http_client_set_content_type: * @http_client: a #MegaHttpClient * @content_type: Content type. * * Set content type header. */ void mega_http_client_set_content_type(MegaHttpClient* http_client, const gchar* content_type) { g_return_if_fail(MEGA_IS_HTTP_CLIENT(http_client)); g_return_if_fail(content_type != NULL); mega_http_client_set_header(http_client, "Content-Type", content_type); }
static gboolean do_send_headers(MegaHttpClient* http_client, GCancellable* cancellable, GError** err) { GError* local_err = NULL; GString* headers; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), FALSE); g_return_val_if_fail(err == NULL || *err == NULL, FALSE); MegaHttpClientPrivate* priv = http_client->priv; headers = g_string_sized_new(300); mega_http_client_set_header(http_client, "Host", priv->host); mega_http_client_set_content_length(http_client, priv->expected_write_count); g_string_append_printf(headers, "%s %s HTTP/1.1\r\n", "POST", priv->resource); g_hash_table_foreach(priv->request_headers, (GHFunc)add_header, headers); g_string_append(headers, "\r\n"); gboolean rs = g_output_stream_write_all(priv->ostream, headers->str, headers->len, NULL, cancellable, &local_err); if (!rs) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_CONNECTION_BROKEN, "Can't write request headers: %s", local_err ? local_err->message : "unknown error"); g_clear_error(&local_err); } g_string_free(headers, TRUE); return rs; }
/** * mega_http_client_set_content_length: * @http_client: a #MegaHttpClient * @content_length: Content length. * * Set content length header. */ void mega_http_client_set_content_length(MegaHttpClient* http_client, guint64 content_length) { g_return_if_fail(MEGA_IS_HTTP_CLIENT(http_client)); gchar* tmp = g_strdup_printf("%" G_GUINT64_FORMAT, content_length); mega_http_client_set_header(http_client, "Content-Length", tmp); g_free(tmp); }
static gboolean server_wants_to_close(MegaHttpClient* http_client) { g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), FALSE); const gchar* connection = g_hash_table_lookup(http_client->priv->response_headers, "Connection"); return !connection || g_ascii_strcasecmp(connection, "close") == 0; }
/** * mega_http_client_set_header: * @http_client: a #MegaHttpClient * @name: Header name (case sensitive) * @value: (allow-none): Header value. Pass null to remove the header. * * Set request header. */ void mega_http_client_set_header(MegaHttpClient* http_client, const gchar* name, const gchar* value) { g_return_if_fail(MEGA_IS_HTTP_CLIENT(http_client)); g_return_if_fail(name != NULL); if (value) g_hash_table_insert(http_client->priv->request_headers, g_strdup(name), g_strdup(value)); else g_hash_table_remove(http_client->priv->request_headers, name); }
/** * mega_http_client_post: * @http_client: a #MegaHttpClient * @url: URL to make the POST to. * @request_length: Length of the request body. * @err: Error. * * Start a new POST request. * * Returns: (transfer full): IO stream you'd use to write request body and read * response. */ MegaHttpIOStream* mega_http_client_post(MegaHttpClient* http_client, const gchar* url, gint64 request_length, GError** err) { GError* local_err = NULL; gchar* host = NULL; gchar* resource = NULL; guint16 port = 80; gboolean https = FALSE; gboolean reconnect = FALSE; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), NULL); g_return_val_if_fail(url != NULL, NULL); g_return_val_if_fail(err == NULL || *err == NULL, NULL); MegaHttpClientPrivate* priv = http_client->priv; // parse URL if (!parse_url(http_client, url, &https, &host, &port, &resource)) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Invalid URL: %s", url); return NULL; } g_rec_mutex_lock(priv->lock); // check that there is a change in host or https flag if (priv->host == NULL || g_ascii_strcasecmp(priv->host, host) || priv->https != https || priv->port != port) { g_free(priv->host); priv->host = host; priv->https = https; priv->port = port; // host/port/ssl changed, reconnection is necessary goto_state(http_client, CONN_STATE_NONE, NULL, NULL); } g_free(priv->resource); priv->resource = resource; if (!goto_state(http_client, CONN_STATE_INIT_CONNECTED, NULL, &local_err)) { g_propagate_error(err, local_err); g_rec_mutex_unlock(priv->lock); return NULL; } priv->request_length = request_length; priv->expected_write_count = request_length; priv->expected_read_count = -1; priv->response_length = -1; g_rec_mutex_unlock(priv->lock); return mega_http_io_stream_new(http_client); }
static gboolean parse_url(MegaHttpClient* http_client, const gchar* url, gboolean* https, gchar** host, guint16* port, gchar** resource) { GMatchInfo *match_info = NULL; gchar* schema = NULL; gchar* port_str = NULL; gboolean status = FALSE; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), FALSE); g_return_val_if_fail(url != NULL, FALSE); g_return_val_if_fail(https != NULL, FALSE); g_return_val_if_fail(host != NULL, FALSE); g_return_val_if_fail(port != NULL, FALSE); g_return_val_if_fail(resource != NULL, FALSE); if (!g_regex_match(http_client->priv->regex_url, url, 0, &match_info)) goto out; // check schema schema = g_match_info_fetch(match_info, 1); if (!g_ascii_strcasecmp("http", schema)) { *port = 80; *https = FALSE; } else if (!g_ascii_strcasecmp("https", schema)) { *port = 443; *https = TRUE; } else goto out; *host = g_match_info_fetch(match_info, 2); port_str = g_match_info_fetch(match_info, 3); if (port_str) { if (*port_str) *port = atoi(port_str); g_free(port_str); } *resource = g_match_info_fetch(match_info, 4); if (*resource == NULL) *resource = g_strdup("/"); status = TRUE; out: g_free(schema); g_match_info_free(match_info); return status; }
/** * mega_http_client_write: * @http_client: a #MegaHttpClient * @buffer: * @count: * @cancellable: * @err: * * Description... * * Returns: */ gssize mega_http_client_write(MegaHttpClient* http_client, const guchar* buffer, gsize count, GCancellable* cancellable, GError** err) { GError* local_err = NULL; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), -1); g_return_val_if_fail(buffer != NULL, -1); g_return_val_if_fail(count > 0, -1); g_return_val_if_fail(err == NULL || *err == NULL, -1); MegaHttpClientPrivate* priv = http_client->priv; g_rec_mutex_lock(priv->lock); if (!goto_state(http_client, CONN_STATE_HEADERS_SENT, cancellable, &local_err)) { g_propagate_error(err, local_err); g_rec_mutex_unlock(priv->lock); return -1; } if (priv->expected_write_count >= 0 && count > priv->expected_write_count) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Write of %" G_GSIZE_FORMAT " too big, expected at most: %" G_GINT64_FORMAT, count, priv->expected_write_count); g_rec_mutex_unlock(priv->lock); return -1; } gssize bytes_written = g_output_stream_write(priv->ostream, buffer, count, cancellable, &local_err); if (bytes_written >= 0) { if (priv->expected_write_count >= 0) priv->expected_write_count -= bytes_written; } if (bytes_written < 0) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_CONNECTION_BROKEN, "Can't write request: %s", local_err ? local_err->message : "unknown error"); g_clear_error(&local_err); goto_state(http_client, CONN_STATE_FAILED, NULL, NULL); } g_rec_mutex_unlock(priv->lock); return bytes_written; }
static gboolean do_connect(MegaHttpClient* http_client, GCancellable* cancellable, GError** err) { GError* local_err = NULL; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), FALSE); g_return_val_if_fail(err == NULL || *err == NULL, FALSE); MegaHttpClientPrivate* priv = http_client->priv; do_disconnect(http_client); // enable/disable TLS if (priv->https) { if (!g_tls_backend_supports_tls(g_tls_backend_get_default())) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "TLS backend not found, please install glib-networking."); return FALSE; } g_socket_client_set_tls(priv->client, TRUE); } else { g_socket_client_set_tls(priv->client, FALSE); } priv->conn = g_socket_client_connect_to_host(priv->client, priv->host, priv->port, cancellable, &local_err); if (!priv->conn) { g_propagate_prefixed_error(err, local_err, "Connection failed: "); return FALSE; } GDataInputStream* data_stream = g_data_input_stream_new(g_io_stream_get_input_stream(G_IO_STREAM(http_client->priv->conn))); g_data_input_stream_set_newline_type(data_stream, G_DATA_STREAM_NEWLINE_TYPE_ANY); priv->istream = G_INPUT_STREAM(data_stream); priv->ostream = g_object_ref(g_io_stream_get_output_stream(G_IO_STREAM(http_client->priv->conn))); return TRUE; }
static void do_disconnect(MegaHttpClient* http_client) { g_return_if_fail(MEGA_IS_HTTP_CLIENT(http_client)); MegaHttpClientPrivate* priv = http_client->priv; if (priv->istream) g_object_unref(priv->istream); if (priv->ostream) g_object_unref(priv->ostream); if (priv->conn) g_object_unref(priv->conn); priv->conn = NULL; priv->istream = NULL; priv->ostream = NULL; }
/** * mega_http_client_close: * @http_client: a #MegaHttpClient * @cancellable: * @err: * * Description... * * Returns: */ gboolean mega_http_client_close(MegaHttpClient* http_client, gboolean force, GCancellable* cancellable, GError** err) { GError* local_err = NULL; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), FALSE); g_return_val_if_fail(err == NULL || *err == NULL, FALSE); MegaHttpClientPrivate* priv = http_client->priv; if (priv->conn_state == CONN_STATE_NONE_CONNECTED && !force) return TRUE; if (!goto_state(http_client, CONN_STATE_NONE, cancellable, &local_err)) { g_propagate_error(err, local_err); return FALSE; } return TRUE; }
/** * mega_http_client_read: * @http_client: a #MegaHttpClient * @buffer: * @count: * @cancellable: * @err: * * Description... * * Returns: */ gssize mega_http_client_read(MegaHttpClient* http_client, guchar* buffer, gsize count, GCancellable* cancellable, GError** err) { GError* local_err = NULL; gssize bytes_read = 0; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), -1); g_return_val_if_fail(buffer != NULL, -1); g_return_val_if_fail(count > 0, -1); g_return_val_if_fail(err == NULL || *err == NULL, -1); MegaHttpClientPrivate* priv = http_client->priv; g_rec_mutex_lock(priv->lock); if (priv->conn_state == CONN_STATE_NONE || priv->conn_state == CONN_STATE_NONE_CONNECTED) goto return0; if (!goto_state(http_client, CONN_STATE_HEADERS_RECEIVED, cancellable, &local_err)) goto err; gint end_state = server_wants_to_close(http_client) ? CONN_STATE_NONE : CONN_STATE_NONE_CONNECTED; // end of stream if (priv->expected_read_count == 0) { if (!goto_state(http_client, end_state, cancellable, &local_err)) goto err; goto return0; } // read at most expected read count if set if (priv->expected_read_count > 0 && count > priv->expected_read_count) count = priv->expected_read_count; // do the reading bytes_read = g_input_stream_read(priv->istream, buffer, count, cancellable, &local_err); if (bytes_read >= 0) { if (priv->expected_read_count > 0) priv->expected_read_count -= bytes_read; } if (priv->expected_read_count == 0) { if (!goto_state(http_client, end_state, cancellable, &local_err)) goto err; } if (bytes_read < 0) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_CONNECTION_BROKEN, "Can't read response: %s", local_err ? local_err->message : "unknown error"); g_clear_error(&local_err); goto_state(http_client, CONN_STATE_FAILED, NULL, NULL); } return0: g_rec_mutex_unlock(priv->lock); return bytes_read; err: g_rec_mutex_unlock(priv->lock); g_propagate_error(err, local_err); return -1; }
/** * mega_http_client_post_simple: * @http_client: a #MegaHttpClient * @url: URL to make the POST to. * @body: (in) (element-type guint8) (array length=body_len) (transfer none): POST request body. * @body_len: Length of the POST request body. * @err: Error. * * Make a POST request. Simplified interface. * * Returns: (transfer full): Response body. */ GString* mega_http_client_post_simple(MegaHttpClient* http_client, const gchar* url, const gchar* body, gssize body_len, GError** err) { GError* local_err = NULL; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), NULL); g_return_val_if_fail(url != NULL, NULL); g_return_val_if_fail(body != NULL, NULL); g_return_val_if_fail(err == NULL || *err == NULL, NULL); MegaHttpClientPrivate* priv = http_client->priv; body_len = body_len >= 0 ? body_len : strlen(body); MegaHttpIOStream* io = mega_http_client_post(http_client, url, body_len, &local_err); if (!io) { g_propagate_error(err, local_err); return NULL; } GInputStream* is = g_io_stream_get_input_stream(G_IO_STREAM(io)); GOutputStream* os = g_io_stream_get_output_stream(G_IO_STREAM(io)); g_rec_mutex_lock(priv->lock); if (body_len > 0) { if (!g_output_stream_write_all(os, body, body_len, NULL, NULL, &local_err)) { g_propagate_error(err, local_err); goto err0; } } if (!goto_state(http_client, CONN_STATE_HEADERS_RECEIVED, NULL, &local_err)) { g_propagate_error(err, local_err); goto err0; } gint64 response_length = mega_http_client_get_response_length(http_client, NULL, &local_err); if (response_length < 0) { g_propagate_prefixed_error(err, local_err, "Response length not set: "); goto err0; } if (response_length > 32 * MB) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Response length over 32 MiB not supported (for post_simple): %s", url); goto err0; } gsize len = (gsize)response_length; GString* response = g_string_sized_new(len); if (len > 0) { if (!g_input_stream_read_all(is, response->str, len, &response->len, NULL, &local_err)) { g_rec_mutex_unlock(priv->lock); g_propagate_error(err, local_err); goto err1; } if (len != response->len) { g_rec_mutex_unlock(priv->lock); g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Can't read the entire response: %s", url); goto err1; } response->str[response->len] = '\0'; } g_rec_mutex_unlock(priv->lock); g_object_unref(io); return response; err1: g_string_free(response, TRUE); err0: g_object_unref(io); g_rec_mutex_unlock(priv->lock); return NULL; }
/* * HTTP client state machine: * * API can request only certain state transitions, others will result in error. * * - none -> init-connected * - none-connected -> init-connected * - init-connected -> headers-sent -> body-sent -> headers-received (any combinations in the right direction) * - headers-received -> (none | none-connected) * - [any] -> none * - [any] -> failed * * Any other requests will fail. * * Also depending on the number of bytes read/written some transitions may * fail. */ static gboolean goto_state(MegaHttpClient* http_client, gint target_state, GCancellable* cancellable, GError** err) { GError* local_err = NULL; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), FALSE); g_return_val_if_fail(target_state >= CONN_STATE_NONE && target_state <= CONN_STATE_FAILED, FALSE); g_return_val_if_fail(err == NULL || *err == NULL, FALSE); MegaHttpClientPrivate* priv = http_client->priv; //g_print("GOTO %d -> %d\n", priv->conn_state, target_state); // we can always transition to NONE/FAILED states by disconnecting if (target_state == CONN_STATE_NONE || target_state == CONN_STATE_FAILED) { do_disconnect(http_client); priv->conn_state = target_state; return TRUE; } // perform connection if (target_state == CONN_STATE_INIT_CONNECTED) { if (priv->conn_state != CONN_STATE_NONE && priv->conn_state != CONN_STATE_NONE_CONNECTED && priv->conn_state != CONN_STATE_FAILED) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Can't connect now"); goto err; } if (priv->conn_state == CONN_STATE_NONE || priv->conn_state == CONN_STATE_FAILED) { if (!do_connect(http_client, cancellable, &local_err)) { g_propagate_error(err, local_err); goto err; } } priv->conn_state = target_state; return TRUE; } // we can't do nothing else in a failed state if (priv->conn_state == CONN_STATE_FAILED) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Request is in the failed state"); goto err; } // we can get from NONE and NONE_CONNECTED only to INIT_CONNECTED by direct // request if (priv->conn_state == CONN_STATE_NONE_CONNECTED || priv->conn_state == CONN_STATE_NONE) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "There's no request being done!"); goto err; } // possible start states: INIT_CONNECTED, HEADERS_SENT, BODY_SENT, HEADERS_RECEIVED // possible target states: HEADERS_SENT, BODY_SENT, HEADERS_RECEIVED, NONE_CONNECTED // check direction of the request if (target_state < priv->conn_state) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Unsupported state transition!"); goto err; } // loop until we reach a desired state or error while (priv->conn_state != target_state) { // move to the next state if possible, otherwise err out if (priv->conn_state == CONN_STATE_INIT_CONNECTED) { if (!do_send_headers(http_client, cancellable, &local_err)) { g_propagate_error(err, local_err); goto err; } priv->conn_state = CONN_STATE_HEADERS_SENT; } else if (priv->conn_state == CONN_STATE_HEADERS_SENT) { if (priv->expected_write_count != 0) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Request body is not finished"); goto err; } priv->conn_state = CONN_STATE_BODY_SENT; } else if (priv->conn_state == CONN_STATE_BODY_SENT) { if (!do_receive_headers(http_client, cancellable, &local_err)) { g_propagate_error(err, local_err); goto err; } priv->conn_state = CONN_STATE_HEADERS_RECEIVED; } else if (priv->conn_state == CONN_STATE_HEADERS_RECEIVED) { if (priv->expected_read_count != 0) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Response body is not finished"); goto err; } priv->conn_state = CONN_STATE_NONE_CONNECTED; } else { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Unhandled state: %d", priv->conn_state); goto err; } } return TRUE; err: do_disconnect(http_client); priv->conn_state = CONN_STATE_FAILED; return FALSE; }
static gboolean do_receive_headers(MegaHttpClient* http_client, GCancellable* cancellable, GError** err) { GError* local_err = NULL; gboolean got_content_length = FALSE; gint line = 0; g_return_val_if_fail(MEGA_IS_HTTP_CLIENT(http_client), FALSE); g_return_val_if_fail(err == NULL || *err == NULL, FALSE); MegaHttpClientPrivate* priv = http_client->priv; g_hash_table_remove_all(priv->response_headers); while (TRUE) { gchar* header = g_data_input_stream_read_line(G_DATA_INPUT_STREAM(priv->istream), NULL, cancellable, &local_err); if (header == NULL) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_CONNECTION_BROKEN, "Can't read response headers: %s", local_err ? local_err->message : "unknown error"); g_clear_error(&local_err); goto err; } if (line == 0) { gint status; gchar* message; if (!parse_http_status(http_client, header, &status, &message)) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Can't read response status: %s", header); g_free(header); goto err; } if (status != 200 && status != 201) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Server returned status %d: %s", status, message); g_free(header); g_free(message); goto err; } g_free(message); } else { if (*header == '\0') { // end of header g_free(header); break; } else { gchar* colon = strchr(header, ':'); if (colon) { *colon = '\0'; gchar* name = g_strstrip(g_ascii_strdown(header, -1)); gchar* value = g_strstrip(g_strdup(colon + 1)); if (!strcmp(name, "content-length")) { priv->expected_read_count = atoi(value); priv->response_length = priv->expected_read_count; got_content_length = TRUE; } g_hash_table_insert(http_client->priv->response_headers, name, value); } else { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "Invalid response header: %s", header); g_free(header); goto err; } } } g_free(header); line++; } if (!got_content_length) { g_set_error(err, MEGA_HTTP_CLIENT_ERROR, MEGA_HTTP_CLIENT_ERROR_OTHER, "We need content length from the server!"); goto err; } return TRUE; err: return FALSE; }