/* * auc - attendedsysUpgrade CLI * Copyright (C) 2017 Daniel Golle * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 * as published by the Free Software Foundation * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #define _GNU_SOURCE #define AUC_VERSION "0.1.4" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define REQ_TIMEOUT 15 #define APIOBJ_CHECK "api/upgrade-check" #define APIOBJ_REQUEST "api/upgrade-request" #define PUBKEY_PATH "/etc/opkg/keys" #ifdef AUC_DEBUG #define DPRINTF(...) if (debug) fprintf(stderr, __VA_ARGS__) #else #define DPRINTF(...) #endif static const char server_issues[]="https://github.com/aparcar/attendedsysupgrade-server/issues"; static char user_agent[80]; static char *serverurl; static int upgrade_packages; static struct ustream_ssl_ctx *ssl_ctx; static const struct ustream_ssl_ops *ssl_ops; static off_t out_bytes; static off_t out_len; static off_t out_offset; static bool cur_resume; static int output_fd = -1; static int retry, imagebuilder, building, ibready; static char *board_name = NULL; static char *target = NULL; static char *distribution = NULL, *version = NULL, *revision = NULL; static int uptodate; static char *filename = NULL; static int rc; #ifdef AUC_DEBUG static int debug = 0; #endif /* * policy for ubus call system board * see procd/system.c */ enum { BOARD_BOARD_NAME, BOARD_RELEASE, __BOARD_MAX, }; static const struct blobmsg_policy board_policy[__BOARD_MAX] = { [BOARD_BOARD_NAME] = { .name = "board_name", .type = BLOBMSG_TYPE_STRING }, [BOARD_RELEASE] = { .name = "release", .type = BLOBMSG_TYPE_TABLE }, }; /* * policy for release information in system board reply * see procd/system.c */ enum { RELEASE_DISTRIBUTION, RELEASE_VERSION, RELEASE_REVISION, RELEASE_TARGET, __RELEASE_MAX, }; static const struct blobmsg_policy release_policy[__RELEASE_MAX] = { [RELEASE_DISTRIBUTION] = { .name = "distribution", .type = BLOBMSG_TYPE_STRING }, [RELEASE_VERSION] = { .name = "version", .type = BLOBMSG_TYPE_STRING }, [RELEASE_REVISION] = { .name = "revision", .type = BLOBMSG_TYPE_STRING }, [RELEASE_TARGET] = { .name = "target", .type = BLOBMSG_TYPE_STRING }, }; /* * policy for packagelist * see rpcd/sys.c */ enum { PACKAGELIST_PACKAGES, __PACKAGELIST_MAX, }; static const struct blobmsg_policy packagelist_policy[__PACKAGELIST_MAX] = { [PACKAGELIST_PACKAGES] = { .name = "packages", .type = BLOBMSG_TYPE_TABLE }, }; /* * policy for upgrade_test * see rpcd/sys.c */ enum { UPGTEST_CODE, UPGTEST_STDERR, __UPGTEST_MAX, }; static const struct blobmsg_policy upgtest_policy[__UPGTEST_MAX] = { [UPGTEST_CODE] = { .name = "code", .type = BLOBMSG_TYPE_INT32 }, [UPGTEST_STDERR] = { .name = "stderr", .type = BLOBMSG_TYPE_STRING }, }; /* * policy to extract version from upgrade-check response */ enum { CHECK_VERSION, CHECK_UPGRADES, __CHECK_MAX, }; static const struct blobmsg_policy check_policy[__CHECK_MAX] = { [CHECK_VERSION] = { .name = "version", .type = BLOBMSG_TYPE_STRING }, [CHECK_UPGRADES] = { .name = "upgrades", .type = BLOBMSG_TYPE_TABLE }, }; static const struct blobmsg_policy pkg_upgrades_policy[2] = { { .type = BLOBMSG_TYPE_STRING }, { .type = BLOBMSG_TYPE_STRING }, }; /* * policy for upgrade-request response * parse download information for the ready image. */ enum { IMAGE_REQHASH, IMAGE_FILESIZE, IMAGE_URL, IMAGE_CHECKSUM, IMAGE_FILES, IMAGE_SYSUPGRADE, __IMAGE_MAX, }; static const struct blobmsg_policy image_policy[__IMAGE_MAX] = { [IMAGE_REQHASH] = { .name = "request_hash", .type = BLOBMSG_TYPE_STRING }, [IMAGE_FILES] = { .name = "files", .type = BLOBMSG_TYPE_STRING }, [IMAGE_SYSUPGRADE] = { .name = "sysupgrade", .type = BLOBMSG_TYPE_STRING }, }; /* * policy for HTTP headers received from server */ enum { H_RANGE, H_LEN, H_IBSTATUS, H_IBQUEUEPOS, H_UNKNOWN_PACKAGE, __H_MAX }; static const struct blobmsg_policy policy[__H_MAX] = { [H_RANGE] = { .name = "content-range", .type = BLOBMSG_TYPE_STRING }, [H_LEN] = { .name = "content-length", .type = BLOBMSG_TYPE_STRING }, [H_IBSTATUS] = { .name = "x-imagebuilder-status", .type = BLOBMSG_TYPE_STRING }, [H_IBQUEUEPOS] = { .name = "x-build-queue-position", .type = BLOBMSG_TYPE_STRING }, [H_UNKNOWN_PACKAGE] = { .name = "x-unknown-package", .type = BLOBMSG_TYPE_STRING }, }; /* * load serverurl from UCI */ static int load_config() { struct uci_context *uci_ctx; struct uci_package *uci_attendedsysupgrade; struct uci_section *uci_s; uci_ctx = uci_alloc_context(); if (!uci_ctx) return -1; uci_ctx->flags &= ~UCI_FLAG_STRICT; if (uci_load(uci_ctx, "attendedsysupgrade", &uci_attendedsysupgrade) || !uci_attendedsysupgrade) { fprintf(stderr, "Failed to load attendedsysupgrade config\n"); return -1; } uci_s = uci_lookup_section(uci_ctx, uci_attendedsysupgrade, "server"); if (!uci_s) { fprintf(stderr, "Failed to read server url from config\n"); return -1; } serverurl = strdup(uci_lookup_option_string(uci_ctx, uci_s, "url")); uci_s = uci_lookup_section(uci_ctx, uci_attendedsysupgrade, "client"); if (!uci_s) { fprintf(stderr, "Failed to read client config\n"); return -1; } upgrade_packages = atoi(uci_lookup_option_string(uci_ctx, uci_s, "upgrade_packages")); uci_free_context(uci_ctx); return 0; } /** * UBUS response callbacks */ /* * rpc-sys packagelist * append packagelist response to blobbuf given in req->priv */ static void pkglist_check_cb(struct ubus_request *req, int type, struct blob_attr *msg) { struct blob_buf *buf = (struct blob_buf *)req->priv; struct blob_attr *tb[__PACKAGELIST_MAX]; blobmsg_parse(packagelist_policy, __PACKAGELIST_MAX, tb, blob_data(msg), blob_len(msg)); if (!tb[PACKAGELIST_PACKAGES]) { fprintf(stderr, "No packagelist received\n"); rc=-1; return; } blobmsg_add_field(buf, BLOBMSG_TYPE_TABLE, "installed", blobmsg_data(tb[PACKAGELIST_PACKAGES]), blobmsg_data_len(tb[PACKAGELIST_PACKAGES])); }; /* * rpc-sys packagelist * append array of package names to blobbuf given in req->priv */ static void pkglist_req_cb(struct ubus_request *req, int type, struct blob_attr *msg) { struct blob_buf *buf = (struct blob_buf *)req->priv; struct blob_attr *tb[__PACKAGELIST_MAX]; struct blob_attr *cur; int rem; void *array; blobmsg_parse(packagelist_policy, __PACKAGELIST_MAX, tb, blob_data(msg), blob_len(msg)); if (!tb[PACKAGELIST_PACKAGES]) { fprintf(stderr, "No packagelist received\n"); return; } array = blobmsg_open_array(buf, "packages"); blobmsg_for_each_attr(cur, tb[PACKAGELIST_PACKAGES], rem) blobmsg_add_string(buf, NULL, blobmsg_name(cur)); blobmsg_close_array(buf, array); }; /* * system board * append append board information to blobbuf given in req->priv * populate board and release global strings */ static void board_cb(struct ubus_request *req, int type, struct blob_attr *msg) { struct blob_buf *buf = (struct blob_buf *)req->priv; struct blob_attr *tb[__BOARD_MAX]; struct blob_attr *rel[__RELEASE_MAX]; blobmsg_parse(board_policy, __BOARD_MAX, tb, blob_data(msg), blob_len(msg)); if (!tb[BOARD_BOARD_NAME]) { fprintf(stderr, "No board name received\n"); rc=-1; return; } board_name = strdup(blobmsg_get_string(tb[BOARD_BOARD_NAME])); if (!tb[BOARD_RELEASE]) { fprintf(stderr, "No release received\n"); rc=-1; return; } blobmsg_parse(release_policy, __RELEASE_MAX, rel, blobmsg_data(tb[BOARD_RELEASE]), blobmsg_data_len(tb[BOARD_RELEASE])); if (!rel[RELEASE_TARGET] || !rel[RELEASE_DISTRIBUTION] || !rel[RELEASE_VERSION] || !rel[RELEASE_REVISION]) { fprintf(stderr, "No release information received\n"); rc=-1; return; } target = strdup(blobmsg_get_string(rel[RELEASE_TARGET])); distribution = strdup(blobmsg_get_string(rel[RELEASE_DISTRIBUTION])); version = strdup(blobmsg_get_string(rel[RELEASE_VERSION])); revision = strdup(blobmsg_get_string(rel[RELEASE_REVISION])); blobmsg_add_string(buf, "distro", distribution); blobmsg_add_string(buf, "target", target); blobmsg_add_string(buf, "version", version); blobmsg_add_string(buf, "revision", revision); } /* * rpc-sys upgrade_test * check if downloaded file is accepted by sysupgrade */ static void upgtest_cb(struct ubus_request *req, int type, struct blob_attr *msg) { int *valid = (int *)req->priv; struct blob_attr *tb[__UPGTEST_MAX]; blobmsg_parse(upgtest_policy, __UPGTEST_MAX, tb, blob_data(msg), blob_len(msg)); if (!tb[UPGTEST_CODE]) { fprintf(stderr, "No sysupgrade test return code received\n"); return; } *valid = (blobmsg_get_u32(tb[UPGTEST_CODE]) == 0)?1:0; if (tb[UPGTEST_STDERR]) fprintf(stderr, "%s", blobmsg_get_string(tb[UPGTEST_STDERR])); else if (*valid == 0) fprintf(stderr, "image verification failed\n"); else fprintf(stderr, "image verification succeeded\n"); }; /** * uclient stuff */ static int open_output_file(const char *path, uint64_t resume_offset) { char *filename = NULL; int flags; int ret; if (cur_resume) flags = O_RDWR; else flags = O_WRONLY | O_EXCL; flags |= O_CREAT; filename = uclient_get_url_filename(path, "firmware.bin"); fprintf(stderr, "Writing to '%s'\n", filename); ret = open(filename, flags, 0644); if (ret < 0) goto free; if (resume_offset && lseek(ret, resume_offset, SEEK_SET) < 0) { fprintf(stderr, "Failed to seek %"PRIu64" bytes in output file\n", resume_offset); close(ret); ret = -1; goto free; } out_offset = resume_offset; out_bytes += resume_offset; free: free(filename); return ret; } struct jsonblobber { json_tokener *tok; struct blob_buf *outbuf; }; static void request_done(struct uclient *cl) { struct jsonblobber *jsb = (struct jsonblobber *)cl->priv; if (jsb) { json_tokener_free(jsb->tok); free(jsb); }; uclient_disconnect(cl); uloop_end(); } static void header_done_cb(struct uclient *cl) { struct blob_attr *tb[__H_MAX]; uint64_t resume_offset = 0, resume_end, resume_size; char *ibstatus; unsigned int queuepos = 0; if (uclient_http_redirect(cl)) { fprintf(stderr, "Redirected to %s on %s\n", cl->url->location, cl->url->host); return; } if (cl->status_code == 204 && cur_resume) { /* Resume attempt failed, try normal download */ cur_resume = false; //init_request(cl); return; } DPRINTF("headers:\n%s\n", blobmsg_format_json_indent(cl->meta, true, 0)); blobmsg_parse(policy, __H_MAX, tb, blob_data(cl->meta), blob_len(cl->meta)); switch (cl->status_code) { case 400: request_done(cl); rc=-1; break; case 409: fprintf(stderr, "Conflicting packages requested\n"); request_done(cl); rc=-2; break; case 412: fprintf(stderr, "%s target %s (%s) not found. Please report this at %s\n", distribution, target, board_name, server_issues); request_done(cl); rc=-2; break; case 413: fprintf(stderr, "image too big.\n"); rc=-1; request_done(cl); break; case 416: fprintf(stderr, "File download already fully retrieved; nothing to do.\n"); request_done(cl); break; case 422: fprintf(stderr, "unknown package '%s' requested.\n", blobmsg_get_string(tb[H_UNKNOWN_PACKAGE])); rc=-1; request_done(cl); break; case 501: fprintf(stderr, "ImageBuilder didn't produce sysupgrade file.\n"); rc=-2; request_done(cl); break; case 204: fprintf(stdout, "system is up to date.\n"); uptodate=1; request_done(cl); break; case 206: if (!cur_resume) { fprintf(stderr, "Error: Partial content received, full content requested\n"); request_done(cl); break; } if (!tb[H_RANGE]) { fprintf(stderr, "Content-Range header is missing\n"); break; } if (sscanf(blobmsg_get_string(tb[H_RANGE]), "bytes %"PRIu64"-%"PRIu64"/%"PRIu64, &resume_offset, &resume_end, &resume_size) != 3) { fprintf(stderr, "Content-Range header is invalid\n"); break; } case 202: if (!tb[H_IBSTATUS]) break; ibstatus = blobmsg_get_string(tb[H_IBSTATUS]); if (!strncmp(ibstatus, "queue", 6)) { if (!imagebuilder) { fprintf(stderr, "server is dispatching build job\n"); imagebuilder=1; } else { if (tb[H_IBQUEUEPOS]) { queuepos = atoi(blobmsg_get_string(tb[H_IBQUEUEPOS])); fprintf(stderr, "build is in queue position %u.\n", queuepos); } } retry=1; } else if (!strncmp(ibstatus, "building", 9)) { if (!building) { fprintf(stderr, "server is now building image...\n"); building=1; } retry=1; } else if (!strncmp(ibstatus, "initialize", 11)) { if (!ibready) { fprintf(stderr, "server is setting up ImageBuilder...\n"); ibready=1; } retry=1; } else { fprintf(stderr, "unrecognized remote imagebuilder status '%s'\n", ibstatus); rc=-2; } // fall through case 200: if (cl->priv) break; if (tb[H_LEN]) out_len = strtoul(blobmsg_get_string(tb[H_LEN]), NULL, 10); output_fd = open_output_file(cl->url->location, resume_offset); if (output_fd < 0) { perror("Cannot open output file"); request_done(cl); } break; default: fprintf(stderr, "HTTP error %d\n", cl->status_code); request_done(cl); break; } } static void read_data_cb(struct uclient *cl) { char buf[256]; int len; json_object *jsobj; struct blob_buf *outbuf = NULL; json_tokener *tok = NULL; struct jsonblobber *jsb = (struct jsonblobber *)cl->priv; if (!jsb) { while (1) { len = uclient_read(cl, buf, sizeof(buf)); if (!len) return; out_bytes += len; write(output_fd, buf, len); } return; } outbuf = jsb->outbuf; tok = jsb->tok; while (1) { len = uclient_read(cl, buf, sizeof(buf)); if (!len) break; out_bytes += len; jsobj = json_tokener_parse_ex(tok, buf, len); if (json_tokener_get_error(tok) == json_tokener_continue) continue; if (json_tokener_get_error(tok) != json_tokener_success) break; if (jsobj) { if (json_object_get_type(jsobj) == json_type_object) blobmsg_add_object(outbuf, jsobj); json_object_put(jsobj); break; } } } static void eof_cb(struct uclient *cl) { if (!cl->data_eof && !uptodate) { fprintf(stderr, "Connection reset prematurely\n"); } request_done(cl); } static void handle_uclient_error(struct uclient *cl, int code) { const char *type = "Unknown error"; switch(code) { case UCLIENT_ERROR_CONNECT: type = "Connection failed"; break; case UCLIENT_ERROR_TIMEDOUT: type = "Connection timed out"; break; case UCLIENT_ERROR_SSL_INVALID_CERT: type = "Invalid SSL certificate"; break; case UCLIENT_ERROR_SSL_CN_MISMATCH: type = "Server hostname does not match SSL certificate"; break; default: break; } fprintf(stderr, "Connection error: %s\n", type); request_done(cl); } static const struct uclient_cb check_cb = { .header_done = header_done_cb, .data_read = read_data_cb, .data_eof = eof_cb, .error = handle_uclient_error, }; static int server_request(const char *url, struct blob_buf *inbuf, struct blob_buf *outbuf) { struct uclient *ucl; struct jsonblobber *jsb = NULL; int rc = -1; char *post_data; out_offset = 0; out_bytes = 0; out_len = 0; uloop_init(); ucl = uclient_new(url, NULL, &check_cb); if (outbuf) { jsb = malloc(sizeof(struct jsonblobber)); jsb->outbuf = outbuf; jsb->tok = json_tokener_new(); }; uclient_http_set_ssl_ctx(ucl, ssl_ops, ssl_ctx, 1); ucl->timeout_msecs = REQ_TIMEOUT * 1000; ucl->priv = jsb; rc = uclient_connect(ucl); if (rc) return rc; rc = uclient_http_set_request_type(ucl, inbuf?"POST":"GET"); if (rc) return rc; uclient_http_reset_headers(ucl); uclient_http_set_header(ucl, "User-Agent", user_agent); if (inbuf) { uclient_http_set_header(ucl, "Content-Type", "text/json"); post_data = blobmsg_format_json(inbuf->head, true); uclient_write(ucl, post_data, strlen(post_data)); } rc = uclient_request(ucl); if (rc) return rc; uloop_run(); uloop_done(); uclient_free(ucl); return 0; } /** * ustream-ssl */ static int init_ustream_ssl(void) { void *dlh; glob_t gl; int i; dlh = dlopen("libustream-ssl.so", RTLD_LAZY | RTLD_LOCAL); if (!dlh) return -1; ssl_ops = dlsym(dlh, "ustream_ssl_ops"); if (!ssl_ops) return -1; ssl_ctx = ssl_ops->context_new(false); glob("/etc/ssl/certs/*.crt", 0, NULL, &gl); if (!gl.gl_pathc) return -2; for (i = 0; i < gl.gl_pathc; i++) ssl_ops->context_add_ca_crt_file(ssl_ctx, gl.gl_pathv[i]); return 0; } static int ask_user(void) { fprintf(stderr, "Are you sure you want to continue the upgrade process? [N/y] "); if (getchar() != 'y') return -1; return 0; } static void print_package_updates(struct blob_attr *upgrades) { struct blob_attr *cur; struct blob_attr *tb[2]; int rem; blobmsg_for_each_attr(cur, upgrades, rem) { blobmsg_parse_array(pkg_upgrades_policy, ARRAY_SIZE(policy), tb, blobmsg_data(cur), blobmsg_data_len(cur)); if (!tb[0] || !tb[1]) continue; fprintf(stdout, "\t%s (%s -> %s)\n", blobmsg_name(cur), blobmsg_get_string(tb[1]), blobmsg_get_string(tb[0])); }; } /* this main function is too big... todo: split */ int main(int args, char *argv[]) { static struct blob_buf allpkg, checkbuf, infobuf, reqbuf, imgbuf, upgbuf; struct ubus_context *ctx = ubus_connect(NULL); uint32_t id; int valid, use_get; char url[256]; char *newversion = NULL; struct blob_attr *tb[__IMAGE_MAX]; struct blob_attr *tbc[__CHECK_MAX]; struct stat imgstat; int check_only = 0; unsigned char argc = 1; snprintf(user_agent, sizeof(user_agent), "%s (%s)", argv[0], AUC_VERSION); fprintf(stdout, "%s\n", user_agent); while (argccontext_free(ssl_ctx); freeconfig: free(serverurl); freeubus: ubus_free(ctx); return rc; }