diff options
author | Moriah Waterland <Moriah.Waterland@Sun.COM> | 2009-06-03 20:16:25 -0600 |
---|---|---|
committer | Moriah Waterland <Moriah.Waterland@Sun.COM> | 2009-06-03 20:16:25 -0600 |
commit | 5c51f1241dbbdf2656d0e10011981411ed0c9673 (patch) | |
tree | 0f30a2e38fe4e5d53a5a67264ba548577d82a86f /usr/src/lib/libpkg/common/pkgweb.c | |
parent | 2b79d384d32b4ea1e278466cd9b0f3bb56daae22 (diff) | |
download | illumos-joyent-5c51f1241dbbdf2656d0e10011981411ed0c9673.tar.gz |
6739234 move SVR4 packaging to ONNV gate
Diffstat (limited to 'usr/src/lib/libpkg/common/pkgweb.c')
-rw-r--r-- | usr/src/lib/libpkg/common/pkgweb.c | 3238 |
1 files changed, 3238 insertions, 0 deletions
diff --git a/usr/src/lib/libpkg/common/pkgweb.c b/usr/src/lib/libpkg/common/pkgweb.c new file mode 100644 index 0000000000..56559a0953 --- /dev/null +++ b/usr/src/lib/libpkg/common/pkgweb.c @@ -0,0 +1,3238 @@ +/* + * CDDL HEADER START + * + * The contents of this file are subject to the terms of the + * Common Development and Distribution License (the "License"). + * You may not use this file except in compliance with the License. + * + * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE + * or http://www.opensolaris.org/os/licensing. + * See the License for the specific language governing permissions + * and limitations under the License. + * + * When distributing Covered Code, include this CDDL HEADER in each + * file and include the License file at usr/src/OPENSOLARIS.LICENSE. + * If applicable, add the following below this CDDL HEADER, with the + * fields enclosed by brackets "[]" replaced with your own identifying + * information: Portions Copyright [yyyy] [name of copyright owner] + * + * CDDL HEADER END + */ + +/* + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * Use is subject to license terms. + */ + +/* Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */ +/* All Rights Reserved */ + + +#include <stdio.h> +#include <limits.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <pkglocs.h> +#include <locale.h> +#include <libintl.h> +#include <libgen.h> +#include <signal.h> +#include <sys/stat.h> +#include <sys/statvfs.h> +#include <sys/types.h> +#include <fcntl.h> +#include <dirent.h> +#include <boot_http.h> +#include <errno.h> +#include <ctype.h> +#include <openssl/pkcs7.h> +#include <openssl/ocsp.h> +#include <openssl/pkcs12.h> +#include <openssl/err.h> +#include <openssl/x509.h> +#include <openssl/pem.h> +#include <openssl/evp.h> +#include <openssl/rand.h> +#include <openssl/x509v3.h> +#include "pkglib.h" +#include "pkglibmsgs.h" +#include "pkglocale.h" +#include "keystore.h" +#include "pkgweb.h" +#include "pkgerr.h" +#include "p12lib.h" + +/* fixed format when making an OCSP request */ +#define OCSP_REQUEST_FORMAT \ + "POST %s HTTP/1.0\r\n" \ + "Content-Type: application/ocsp-request\r\n" \ + "Content-Length: %d\r\n\r\n" + +/* + * no security is afforded by using this phrase to "encrypt" CA certificates, + * but it might aid in debugging and has to be non-null + */ +#define WEB_CA_PHRASE "schizophrenic" + +/* This one needs the ': ' at the end */ +#define CONTENT_TYPE_HDR "Content-Type" +#define CONTENT_DISPOSITION_HDR "Content-Disposition" +#define CONTENT_OCSP_RESP "application/ocsp-response" +#define CONTENT_LENGTH_HDR "Content-Length" +#define LAST_MODIFIED_HDR "Last-Modified" +#define OCSP_BUFSIZ 1024 + +/* + * default amount of time that is allowed for error when checking + * OCSP response validity. + * For example, if this is set to 5 minutes, then if a response + * is issued that is valid from 12:00 to 1:00, then we will + * accept it if the local time is between 11:55 and 1:05. + * This takes care of not-quite-synchronized server and client clocks. + */ +#define OCSP_VALIDITY_PERIOD (5 * 60) + +/* this value is defined by getpassphrase(3c) manpage */ +#define MAX_PHRASELEN 257 + +/* Max length of "enter password again" prompt message */ +#define MAX_VERIFY_MSGLEN 1024 + +/* local prototypes */ +static boolean_t remove_dwnld_file(char *); +static boolean_t get_ENV_proxyport(PKG_ERR *, ushort_t *); +static boolean_t make_link(char *, char *); +static WebStatus web_send_request(PKG_ERR *, int, int, int); +static boolean_t web_eval_headers(PKG_ERR *); +static WebStatus web_get_file(PKG_ERR *, char *, int, char **); +static boolean_t ck_dwnld_dir_space(PKG_ERR *, char *, ulong_t); +static WebStatus web_connect(PKG_ERR *); +static boolean_t web_setup(PKG_ERR *); +static boolean_t check_dwnld_dir(PKG_ERR *, char *); +static boolean_t parse_url_proxy(PKG_ERR *, char *, char *, ushort_t); +static boolean_t web_disconnect(void); +static char *get_unique_filename(char *, char *); +static boolean_t get_ENV_proxy(PKG_ERR *, char **); +static char *condense_lastmodified(char *); +static int web_verify(int, X509_STORE_CTX *); +static int get_issuer(X509 **issuer, X509_STORE_CTX *ctx, X509 *x); +static boolean_t get_ocsp_uri(X509 *, char **); +static OCSPStatus ocsp_verify(PKG_ERR *, X509 *, X509 *, char *, url_hport_t *, + STACK_OF(X509) *); +static char *get_time_string(ASN1_GENERALIZEDTIME *); +static char *write_ca_file(PKG_ERR *, char *, STACK_OF(X509) *, char *); +static boolean_t _get_random_info(void *, int); +static boolean_t init_session(void); +static void progress_setup(int, ulong_t); +static void progress_report(int, ulong_t); +static void progress_finish(int); +static char *replace_token(char *, char, char); +static void dequote(char *); +static void trim(char *); + + +/* + * structure used to hold data passed back to the + * X509 verify callback routine in validate_signature() + */ +typedef struct { + url_hport_t *proxy; + PKG_ERR *err; + STACK_OF(X509) *cas; +} verify_cb_data_t; + +/* Progress bar variables */ +static ulong_t const_increment, const_divider, completed, const_completed; + +/* current network backoff wait period */ +static int cur_backoff = 0; + +/* download session context handle */ +static WEB_SESSION *ps; + +static int webpkg_install = 0; +static char *prompt = NULL; +static char *passarg = NULL; + + +/* ~~~~~~~~~~~~~~ Public Functions ~~~~~~~~~~~~~~~~~~~ */ + +/* + * Name: set_prompt + * Description: Specifies the prompt to use with the pkglib + * passphrase callback routine. + * + * Arguments: newprompt - The prompt to display + * + * Returns : NONE + */ +void +set_passphrase_prompt(char *newprompt) +{ + prompt = newprompt; +} + +/* + * Name: set_passarg + * Description: Specifies the passphrase retrieval method + * to use with the pkglib + * passphrase callback routine. + * + * Arguments: newpassarg - The new password retrieval arg + * + * Returns : NONE + */ +void +set_passphrase_passarg(char *newpassarg) +{ + passarg = newpassarg; +} + +/* + * Name: get_proxy_port + * Description: Resolves proxy specification + * + * Arguments: err - where to record any errors. + * proxy - Location to store result - if *proxy is not + * null, then it will be validated, but not changed + * + * Returns : B_TRUE - success, B_FALSE otherwise + * on success, *proxy and *port are set to either + * the user-supplied proxy and port, or the + * ones found in the environment variables + * HTTPPROXY and/or HTTPROXYPORT + */ +boolean_t +get_proxy_port(PKG_ERR *err, char **proxy, ushort_t *port) +{ + if (*proxy != NULL) { + if (!path_valid(*proxy)) { + /* bad proxy supplied */ + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_BAD_PROXY), *proxy); + return (B_FALSE); + } + if (!get_ENV_proxyport(err, port)) { + /* env set, but bad */ + return (B_FALSE); + } + } else { + if (!get_ENV_proxy(err, proxy)) { + /* environment variable set, but bad */ + return (B_FALSE); + } + if ((*proxy != NULL) && !path_valid(*proxy)) { + /* env variable set, but bad */ + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_BAD_PROXY), *proxy); + return (B_FALSE); + } + if (!get_ENV_proxyport(err, port)) { + /* env variable set, but bad */ + return (B_FALSE); + } + } + return (B_TRUE); +} + +/* + * Name: path_valid + * Description: Checks a string for being a valid path + * + * Arguments: path - path to validate + * + * Returns : B_TRUE - success, B_FALSE otherwise. + * B_FALSE means path was null, too long (>PATH_MAX), + * or too short (<1) + */ +boolean_t +path_valid(char *path) +{ + if (path == NULL) { + return (B_FALSE); + } else if (strlen(path) > PATH_MAX) { + return (B_FALSE); + } else if (strlen(path) >= 1) { + return (B_TRUE); + } else { + /* path < 1 */ + return (B_FALSE); + } +} + +/* + * Name: web_cleanup + * Description: Deletes temp files, closes, frees memory taken + * by 'ps' static structure + * + * Arguments: none + * + * Returns : none + */ +void +web_cleanup(void) +{ + PKG_ERR *err; + + if (ps == NULL) + return; + + err = pkgerr_new(); + + if (ps->keystore) { + (void) close_keystore(err, ps->keystore, NULL); + } + + ps->keystore = NULL; + + pkgerr_free(err); + + if (ps->uniqfile) { + (void) remove_dwnld_file(ps->uniqfile); + free(ps->uniqfile); + ps->uniqfile = NULL; + } + if (ps->link) { + (void) remove_dwnld_file(ps->link); + free(ps->link); + ps->link = NULL; + } + if (ps->dwnld_dir) { + (void) rmdir(ps->dwnld_dir); + ps->dwnld_dir = NULL; + } + if (ps->errstr) { + free(ps->errstr); + ps->errstr = NULL; + } + + if (ps->content) { + free(ps->content); + ps->content = NULL; + } + + if (ps->resp) { + http_free_respinfo(ps->resp); + ps->resp = NULL; + } + + if (ps) { + free(ps); + ps = NULL; + } +} + +/* + * Name: web_session_control + * Description: Downloads an arbitrary URL and saves to disk. + * + * Arguments: err - where to record any errors. + * url - URL pointing to content to download - can be + * http:// or https:// + * dwnld_dir - Directory to download into + * keystore - keystore to use for accessing trusted + * certs when downloading using SSL + * proxy - HTTP proxy to use, or NULL for no proxy + * proxy_port - HTTP proxy port to use, ignored + * if proxy is NULL + * passarg - method to retrieve password + * retries - # of times to retry download before + * giving up + * timeout - how long to wait before retrying, + * when download is interrupted + * nointeract - if non-zero, do not output + * download progress to screen + * + * Returns : B_TRUE - success, B_FALSE otherwise + */ +boolean_t +web_session_control(PKG_ERR *err, char *url, char *dwnld_dir, + keystore_handle_t keystore, char *proxy, ushort_t proxy_port, + int retries, int timeout, int nointeract, char **fname) +{ + int i; + boolean_t ret = B_TRUE; + boolean_t retrieved = B_FALSE; + + if (!init_session()) { + ret = B_FALSE; + goto cleanup; + } + + if (!parse_url_proxy(err, url, proxy, proxy_port)) { + ret = B_FALSE; + goto cleanup; + } + + ps->timeout = timeout; + + if (keystore != NULL) + ps->keystore = keystore; + + if (dwnld_dir != NULL) + ps->dwnld_dir = xstrdup(dwnld_dir); + else { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_NO_DWNLD_DIR)); + ret = B_FALSE; + goto cleanup; + } + + if (!check_dwnld_dir(err, dwnld_dir)) { + ret = B_FALSE; + goto cleanup; + } + + for (i = 0; i < retries && !retrieved; i++) { + if (!web_setup(err)) { + ret = B_FALSE; + goto cleanup; + } + + switch (web_connect(err)) { + /* time out and wait a little bit for these failures */ + case WEB_OK: + /* were able to connect */ + reset_backoff(); + break; + case WEB_TIMEOUT: + echo_out(nointeract, gettext(MSG_DWNLD_TIMEOUT)); + (void) web_disconnect(); + backoff(); + continue; + + case WEB_CONNREFUSED: + echo_out(nointeract, gettext(MSG_DWNLD_CONNREF), + ps->url.hport.hostname); + (void) web_disconnect(); + backoff(); + continue; + case WEB_HOSTDOWN: + echo_out(nointeract, gettext(MSG_DWNLD_HOSTDWN), + ps->url.hport.hostname); + (void) web_disconnect(); + backoff(); + continue; + + default: + /* every other failure is a hard failure, so bail */ + ret = B_FALSE; + goto cleanup; + } + + switch (web_send_request(err, HTTP_REQ_TYPE_HEAD, + ps->data.cur_pos, ps->data.content_length)) { + case WEB_OK: + /* were able to connect */ + reset_backoff(); + break; + case WEB_TIMEOUT: + echo_out(nointeract, gettext(MSG_DWNLD_TIMEOUT)); + (void) web_disconnect(); + backoff(); + continue; + + case WEB_CONNREFUSED: + echo_out(nointeract, gettext(MSG_DWNLD_CONNREF), + ps->url.hport.hostname); + (void) web_disconnect(); + backoff(); + continue; + case WEB_HOSTDOWN: + echo_out(nointeract, gettext(MSG_DWNLD_HOSTDWN), + ps->url.hport.hostname); + (void) web_disconnect(); + backoff(); + continue; + default: + /* every other case is failure, so bail */ + ret = B_FALSE; + goto cleanup; + } + + if (!web_eval_headers(err)) { + ret = B_FALSE; + goto cleanup; + } + + switch (web_get_file(err, dwnld_dir, nointeract, fname)) { + case WEB_OK: + /* were able to retrieve file */ + retrieved = B_TRUE; + reset_backoff(); + break; + + case WEB_TIMEOUT: + echo_out(nointeract, gettext(MSG_DWNLD_TIMEOUT)); + (void) web_disconnect(); + backoff(); + continue; + + case WEB_CONNREFUSED: + echo_out(nointeract, gettext(MSG_DWNLD_CONNREF), + ps->url.hport.hostname); + (void) web_disconnect(); + backoff(); + continue; + case WEB_HOSTDOWN: + echo_out(nointeract, gettext(MSG_DWNLD_HOSTDWN), + ps->url.hport.hostname); + (void) web_disconnect(); + backoff(); + continue; + default: + /* every other failure is a hard failure, so bail */ + ret = B_FALSE; + goto cleanup; + } + } + + if (!retrieved) { + /* max retries attempted */ + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_DWNLD_FAILED), retries); + ret = B_FALSE; + } +cleanup: + (void) web_disconnect(); + if (!ret) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_DWNLD), url); + } + return (ret); +} + +/* + * Name: get_signature + * Description: retrieves signature from signed package. + * + * Arguments: err - where to record any errors. + * ids_name - name of package stream, for error reporting + * devp - Device on which package resides that we + * result - where to store resulting PKCS7 signature + * + * Returns : B_TRUE - package is signed and signature returned OR + * package is not signed, in which case result is NULL + * + * B_FALSE - there were problems accessing signature, + * and it is unknown whether it is signed or not. Errors + * recorded in 'err'. + */ +boolean_t +get_signature(PKG_ERR *err, char *ids_name, struct pkgdev *devp, PKCS7 **result) +{ + char path[PATH_MAX]; + int len, fd = -1; + struct stat buf; + FILE *fp = NULL; + boolean_t ret = B_TRUE; + BIO *sig_in = NULL; + PKCS7 *p7 = NULL; + + /* + * look for signature. If one was in the stream, + * it is now extracted + */ + if (((len = snprintf(path, PATH_MAX, "%s/%s", devp->dirname, + SIGNATURE_FILENAME)) >= PATH_MAX) || (len < 0)) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_LEN), ids_name); + ret = B_FALSE; + goto cleanup; + } + + if ((fd = open(path, O_RDONLY|O_NONBLOCK)) == -1) { + /* + * only if the signature is non-existant + * do we "pass" + */ + if (errno != ENOENT) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_OPENSIG), + strerror(errno)); + ret = B_FALSE; + goto cleanup; + } + } else { + /* found sig file. parse it. */ + if (fstat(fd, &buf) == -1) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_OPENSIG), strerror(errno)); + ret = B_FALSE; + goto cleanup; + } + + if (!S_ISREG(buf.st_mode)) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_OPENSIG), + (gettext(ERR_NOT_REG))); + ret = B_FALSE; + goto cleanup; + } + + if ((fp = fdopen(fd, "r")) == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_OPENSIG), strerror(errno)); + ret = B_FALSE; + goto cleanup; + } + + /* + * read in signature. If it's invalid, we + * punt, unless we're ignoring it + */ + if ((sig_in = BIO_new_fp(fp, BIO_NOCLOSE)) == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_OPENSIG), strerror(errno)); + goto cleanup; + } + + if ((p7 = PEM_read_bio_PKCS7(sig_in, + NULL, NULL, NULL)) == NULL) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_CORRUPTSIG), + ids_name); + ret = B_FALSE; + goto cleanup; + } + *result = p7; + p7 = NULL; + } + +cleanup: + if (sig_in) + (void) BIO_free(sig_in); + if (fp) + (void) fclose(fp); + if (fd != -1) + (void) close(fd); + if (p7) + (void) PKCS7_free(p7); + + return (ret); +} + +/* + * Name: echo_out + * Description: Conditionally output a message to stdout + * + * Arguments: nointeract - if non-zero, do not output anything + * fmt - print format + * ... - print arguments + * + * Returns : none + */ +void +echo_out(int nointeract, char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + + if (nointeract) + return; + + (void) vfprintf(stdout, fmt, ap); + + va_end(ap); + + (void) putc('\n', stdout); +} + +/* + * Name: strip_port + * Description: Returns "port" portion of a "hostname:port" string + * + * Arguments: proxy - full "hostname:port" string pointer + * + * Returns : the "port" portion of a "hostname:port" string, + * converted to a decimal integer, or (int)0 + * if string contains no :port suffix. + */ +ushort_t +strip_port(char *proxy) +{ + char *tmp_port; + + if ((tmp_port = strpbrk(proxy, ":")) != NULL) + return (atoi(tmp_port)); + else + return (0); +} + +/* + * Name: set_web_install + * Description: Sets flag indicating we are doing a web-based install + * + * Arguments: none + * + * Returns : none + */ +void +set_web_install(void) +{ + webpkg_install++; +} + +/* + * Name: is_web_install + * Description: Determines whether we are doing a web-based install + * + * Arguments: none + * + * Returns : non-zero if we are doing a web-based install, 0 otherwise + */ +int +is_web_install(void) +{ + return (webpkg_install); +} + +/* ~~~~~~~~~~~~~~ Private Functions ~~~~~~~~~~~~~~~~~~~ */ + +/* + * Name: web_disconnect + * Description: Disconnects connection to web server + * + * Arguments: none + * + * Returns : B_TRUE - successful disconnect, B_FALSE otherwise + * Temp certificiate files are deleted, + * if one was used to initiate the connection + * (such as when using SSL) + */ +static boolean_t +web_disconnect(void) +{ + if (ps->certfile) { + (void) unlink(ps->certfile); + } + if (http_srv_disconnect(ps->hps) == 0) + if (http_srv_close(ps->hps) == 0) + return (B_TRUE); + + return (B_FALSE); +} + +/* + * Name: check_dwnld_dir + * Description: Creates temp download directory + * + * Arguments: err - where to record any errors. + * dwnld_dir - name of directory to create + * + * Returns : B_TRUE - success, B_FALSE otherwise + * on success, directory is created with + * safe permissions + */ +static boolean_t +check_dwnld_dir(PKG_ERR *err, char *dwnld_dir) +{ + DIR *dirp; + + /* + * Check the directory passed in. If it doesn't exist, create it + * with strict permissions + */ + if ((dirp = opendir(dwnld_dir)) == NULL) { + if (mkdir(dwnld_dir, 0744) == -1) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTEMP), + dwnld_dir); + return (B_FALSE); + } + } + if (dirp) { + (void) closedir(dirp); + } + return (B_TRUE); +} + +/* + * Name: ds_validate_signature + * Description: Validates signature found in a package datastream + * + * Arguments: err - where to record any errors. + * pkgdev - Package context handle of package to verify + * pkgs - Null-terminated List of package name to verify + * ids_name - Pathname to stream to validate + * p7 - PKCS7 signature decoded from stream header + * cas - List of trusted CA certificates + * proxy - Proxy to use when doing online validation (OCSP) + * nointeract - if non-zero, do not output to screen + * + * Returns : B_TRUE - success, B_FALSE otherwise + * success means signature was completely validated, + * and contents of stream checked against signature. + */ +boolean_t +ds_validate_signature(PKG_ERR *err, struct pkgdev *pkgdev, char **pkgs, + char *ids_name, PKCS7 *p7, STACK_OF(X509) *cas, + url_hport_t *proxy, int nointeract) +{ + BIO *p7_bio; + boolean_t ret = B_TRUE; + + /* make sure it's a Signed PKCS7 message */ + if (!PKCS7_type_is_signed(p7)) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_CORRUPTSIG_TYPE), + ids_name); + ret = B_FALSE; + goto cleanup; + } + + /* initialize PKCS7 object to be filled in */ + if (!PKCS7_get_detached(p7)) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_CORRUPTSIG_DT), + ids_name); + ret = B_FALSE; + goto cleanup; + } + + /* dump header and packages into BIO to calculate the message digest */ + if ((p7_bio = PKCS7_dataInit(p7, NULL)) == NULL) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_CORRUPTSIG), + ids_name); + ret = B_FALSE; + goto cleanup; + } + + if ((BIO_ds_dump_header(err, p7_bio) != 0) || + (BIO_ds_dump(err, ids_name, p7_bio) != 0)) { + ret = B_FALSE; + goto cleanup; + } + (void) BIO_flush(p7_bio); + + /* validate the stream and its signature */ + if (!validate_signature(err, ids_name, p7_bio, p7, cas, + proxy, nointeract)) { + ret = B_FALSE; + goto cleanup; + } + + /* reset device stream (really bad performance for tapes) */ + (void) ds_close(1); + (void) ds_init(ids_name, pkgs, pkgdev->norewind); + +cleanup: + return (ret); +} + + +/* + * Name: validate_signature + * Description: Validates signature of an arbitrary stream of bits + * + * Arguments: err - where to record any errors. + * name - Descriptive name of object being validated, + * for good error reporting messages + * indata - BIO object to read stream bits from + * p7 - PKCS7 signature of stream + * cas - List of trusted CA certificates + * proxy - Proxy to use when doing online validation (OCSP) + * nointeract - if non-zero, do not output to screen + * + * Returns : B_TRUE - success, B_FALSE otherwise + * success means signature was completely validated, + * and contents of stream checked against signature. + */ +boolean_t +validate_signature(PKG_ERR *err, char *name, BIO *indata, PKCS7 *p7, + STACK_OF(X509) *cas, url_hport_t *proxy, int nointeract) +{ + STACK_OF(PKCS7_SIGNER_INFO) *sec_sinfos = NULL; + + PKCS7_SIGNER_INFO *signer = NULL; + X509_STORE *sec_truststore = NULL; + X509_STORE_CTX *ctx = NULL; + X509 *signer_cert = NULL, *issuer = NULL; + STACK_OF(X509) *chaincerts = NULL; + int i, k; + unsigned long errcode; + const char *err_data = NULL; + const char *err_reason = NULL; + char *err_string; + int err_flags; + verify_cb_data_t verify_data; + char *signer_sname; + char *signer_iname; + PKCS7_ISSUER_AND_SERIAL *ias; + boolean_t ret = B_TRUE; + + /* only support signed PKCS7 signatures */ + if (!PKCS7_type_is_signed(p7)) { + PKCS7err(PKCS7_F_PKCS7_DATAVERIFY, PKCS7_R_WRONG_PKCS7_TYPE); + ret = B_FALSE; + goto cleanup; + } + + /* initialize temporary internal trust store used for verification */ + sec_truststore = X509_STORE_new(); + + for (i = 0; i < sk_X509_num(cas); i++) { + if (X509_STORE_add_cert(sec_truststore, + sk_X509_value(cas, i)) == 0) { + pkgerr_add(err, PKGERR_VERIFY, gettext(ERR_MEM)); + ret = B_FALSE; + goto cleanup; + } + } + + /* get signers from the signature */ + if ((sec_sinfos = PKCS7_get_signer_info(p7)) == NULL) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_CORRUPTSIG), name); + ret = B_FALSE; + goto cleanup; + } + + /* verify each signer found in the PKCS7 signature */ + for (k = 0; k < sk_PKCS7_SIGNER_INFO_num(sec_sinfos); k++) { + signer = sk_PKCS7_SIGNER_INFO_value(sec_sinfos, k); + signer_cert = PKCS7_cert_from_signer_info(p7, signer); + signer_sname = get_subject_display_name(signer_cert); + signer_iname = get_issuer_display_name(signer_cert); + + echo_out(nointeract, gettext(MSG_VERIFY), signer_sname); + + /* find the issuer of the current cert */ + chaincerts = p7->d.sign->cert; + ias = signer->issuer_and_serial; + issuer = X509_find_by_issuer_and_serial(chaincerts, + ias->issuer, ias->serial); + + /* were we not able to find the issuer cert */ + if (issuer == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_VERIFY_ISSUER), + signer_iname, signer_sname); + ret = B_FALSE; + goto cleanup; + } + + /* Lets verify */ + if ((ctx = X509_STORE_CTX_new()) == NULL) { + pkgerr_add(err, PKGERR_VERIFY, gettext(ERR_MEM)); + ret = B_FALSE; + goto cleanup; + } + (void) X509_STORE_CTX_init(ctx, sec_truststore, + issuer, chaincerts); + (void) X509_STORE_CTX_set_purpose(ctx, + X509_PURPOSE_ANY); + + /* callback will perform OCSP on certificates with OCSP data */ + X509_STORE_CTX_set_verify_cb(ctx, web_verify); + + /* pass needed data into callback through the app_data handle */ + verify_data.proxy = proxy; + verify_data.cas = cas; + verify_data.err = err; + (void) X509_STORE_CTX_set_app_data(ctx, &verify_data); + + /* first verify the certificate chain */ + i = X509_verify_cert(ctx); + if (i <= 0 && ctx->error != X509_V_ERR_CERT_HAS_EXPIRED) { + signer_sname = + get_subject_display_name(ctx->current_cert); + signer_iname = + get_issuer_display_name(ctx->current_cert); + /* if the verify context holds an error, print it */ + if (ctx->error != X509_V_OK) { + pkgerr_add(err, PKGERR_VERIFY, + gettext(ERR_VERIFY_SIG), signer_sname, + signer_iname, + (char *)X509_verify_cert_error_string(ctx->error)); + } else { + /* some other error. print them all. */ + while ((errcode = ERR_get_error_line_data(NULL, + NULL, &err_data, &err_flags)) != 0) { + err_reason = + ERR_reason_error_string(errcode); + if (err_reason == NULL) { + err_reason = + gettext(ERR_SIG_INT); + } + + if (!(err_flags & ERR_TXT_STRING)) { + err_data = + gettext(ERR_SIG_INT); + } + err_string = + xmalloc(strlen(err_reason) + + strlen(err_data) + 3); + (void) sprintf(err_string, "%s: %s", + err_reason, err_data); + pkgerr_add(err, PKGERR_VERIFY, + gettext(ERR_VERIFY_SIG), + signer_sname, signer_iname, + err_string); + free(err_string); + } + } + ret = B_FALSE; + goto cleanup; + } + + /* now verify the signature */ + i = PKCS7_signatureVerify(indata, p7, signer, issuer); + + if (i <= 0) { + /* print out any OpenSSL-specific errors */ + signer_sname = + get_subject_display_name(ctx->current_cert); + signer_iname = + get_subject_display_name(ctx->current_cert); + while ((errcode = ERR_get_error_line_data(NULL, + NULL, &err_data, &err_flags)) != 0) { + err_reason = + ERR_reason_error_string(errcode); + if (err_reason == NULL) { + err_reason = + gettext(ERR_SIG_INT); + } + + if (!(err_flags & ERR_TXT_STRING)) { + err_data = + gettext(ERR_SIG_INT); + } + pkgerr_add(err, PKGERR_VERIFY, + gettext(ERR_VERIFY_SIG), signer_sname, + signer_iname, err_reason); + pkgerr_add(err, PKGERR_VERIFY, + gettext(ERR_VERIFY_SIG), signer_sname, + signer_iname, err_data); + } + ret = B_FALSE; + goto cleanup; + } + + echo_out(nointeract, gettext(MSG_VERIFY_OK), signer_sname); + } + + /* signature(s) verified successfully */ +cleanup: + if (ctx) + X509_STORE_CTX_cleanup(ctx); + return (ret); +} + +/* + * Name: web_verify + * Description: Callback used by PKCS7_dataVerify when + * verifying a certificate chain. + * + * Arguments: err - where to record any errors. + * ctx - The context handle of the current verification operation + * + * Returns : B_TRUE - success, B_FALSE otherwise + * if it's '0' (not OK) we simply return it, since the + * verification operation has already determined that the + * cert is invalid. if 'ok' is non-zero, then we do our + * checks, and return 0 or 1 based on if the cert is + * invalid or valid. + */ +static int +web_verify(int ok, X509_STORE_CTX *ctx) +{ + X509 *curr_cert; + X509 *curr_issuer; + char *uri; + url_hport_t *proxy; + PKG_ERR *err = NULL; + STACK_OF(X509) *cas; + if (!ok) { + /* don't override a verify failure */ + return (ok); + } + + + /* get app data supplied through callback context */ + err = ((verify_cb_data_t *)X509_STORE_CTX_get_app_data(ctx))->err; + proxy = ((verify_cb_data_t *)X509_STORE_CTX_get_app_data(ctx))->proxy; + cas = ((verify_cb_data_t *)X509_STORE_CTX_get_app_data(ctx))->cas; + + /* Check revocation status */ + curr_cert = X509_STORE_CTX_get_current_cert(ctx); + + /* this shouldn't happen */ + if (curr_cert == NULL) { + pkgerr_add(err, PKGERR_INTERNAL, gettext(ERR_PKG_INTERNAL), + __FILE__, __LINE__); + return (0); + } + + /* don't perform OCSP unless cert has required OCSP extensions */ + if (get_ocsp_uri(curr_cert, &uri)) { + if (get_issuer(&curr_issuer, ctx, curr_cert) <= 0) { + /* no issuer! */ + pkgerr_add(err, PKGERR_INTERNAL, + gettext(ERR_PKG_INTERNAL), + __FILE__, __LINE__); + return (0); + } + + /* + * ok we have the current cert + * and its issuer. Do the OCSP check + */ + + /* + * OCSP extensions are, by, RFC 2459, never critical + * extensions, therefore, we only fail if we were able + * to explicitly contact an OCSP responder, and that + * responder did not indicate the cert was valid. We + * also fail if user-supplied data could not be parsed + * or we run out of memory. We succeeed for "soft" + * failures, such as not being able to connect to the + * OCSP responder, or trying to use if the OCSP URI + * indicates SSL must be used (which we do not + * support) + */ + switch (ocsp_verify(err, curr_cert, curr_issuer, + uri, proxy, cas)) { + case OCSPMem: /* Ran out of memory */ + case OCSPInternal: /* Some internal error */ + case OCSPVerify: /* OCSP responder indicated fail */ + return (0); + } + /* all other cases are success, or soft failures */ + pkgerr_clear(err); + } + + return (ok); +} + +/* + * Name: get_time_string + * Description: Generates a human-readable string from an ASN1_GENERALIZED_TIME + * + * Arguments: intime - The time to convert + * + * Returns : A pointer to a static string representing the passed-in time. + */ +static char +*get_time_string(ASN1_GENERALIZEDTIME *intime) +{ + + static char time[ATTR_MAX]; + BIO *mem; + char *p; + + if (intime == NULL) { + return (NULL); + } + if ((mem = BIO_new(BIO_s_mem())) == NULL) { + return (NULL); + } + + if (ASN1_GENERALIZEDTIME_print(mem, intime) == 0) { + (void) BIO_free(mem); + return (NULL); + } + + if (BIO_gets(mem, time, ATTR_MAX) <= 0) { + (void) BIO_free(mem); + return (NULL); + } + + (void) BIO_free(mem); + + /* trim the end of the string */ + for (p = time + strlen(time) - 1; isspace(*p); p--) { + *p = '\0'; + } + + return (time); +} + +/* + * Name: get_ocsp_uri + * Description: Examines an X509 certificate and retrieves the embedded + * OCSP Responder URI if one exists. + * + * Arguments: cert - The cert to inspect + * uri - pointer where the newly-allocated URI is placed, if found + * + * Returns : Success if the URI was found. Appropriate status otherwise. + */ +static boolean_t +get_ocsp_uri(X509 *cert, char **uri) +{ + AUTHORITY_INFO_ACCESS *aia; + ACCESS_DESCRIPTION *ad; + int i; + + if (getenv("PKGWEB_TEST_OCSP")) { + *uri = xstrdup(getenv("PKGWEB_TEST_OCSP")); + return (B_TRUE); + } + + /* get the X509v3 extension holding the OCSP URI */ + if ((aia = X509_get_ext_d2i(cert, NID_info_access, + NULL, NULL)) != NULL) { + for (i = 0; i < sk_ACCESS_DESCRIPTION_num(aia); i++) { + ad = sk_ACCESS_DESCRIPTION_value(aia, i); + if (OBJ_obj2nid(ad->method) == NID_ad_OCSP) { + if (ad->location->type == GEN_URI) { + *uri = + xstrdup((char *)ASN1_STRING_data(ad->location->d.ia5)); + return (B_TRUE); + } + } + } + } + + /* no URI was found */ + return (B_FALSE); +} + +/* + * Name: ocsp_verify + * Description: Attempts to contact an OCSP Responder and ascertain the validity + * of an X509 certificate. + * + * Arguments: err - Error object to add error messages to + * cert - The cert to validate + * issuer - The certificate of the issuer of 'cert' + * uri - The OCSP Responder URI + * cas - The trusted CA certificates used to verify the + * signed OCSP response + * Returns : Success - The OCSP Responder reported a 'good' + * status for the cert otherwise, appropriate + * error is returned. + */ +static OCSPStatus +ocsp_verify(PKG_ERR *err, X509 *cert, X509 *issuer, + char *uri, url_hport_t *proxy, STACK_OF(X509) *cas) +{ + OCSP_CERTID *id; + OCSP_REQUEST *req; + OCSP_RESPONSE *resp; + OCSP_BASICRESP *bs; + BIO *cbio, *mem; + char ocspbuf[OCSP_BUFSIZ]; + char *host = NULL, *portstr = NULL, *path = "/", *p, *q, *r; + int port, status, reason; + int len, retval, respcode, use_ssl = 0; + ASN1_GENERALIZEDTIME *rev, *thisupd, *nextupd; + char *subjname; + time_t currtime; + char currtimestr[ATTR_MAX]; + unsigned long errcode; + const char *err_reason; + + subjname = get_subject_display_name(cert); + + /* parse the URI into its constituent parts */ + if (OCSP_parse_url(uri, &host, &portstr, &path, &use_ssl) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_PARSE), uri); + return (OCSPParse); + } + + /* we don't currently support SSL-based OCSP Responders */ + if (use_ssl) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_UNSUP), uri); + return (OCSPUnsupported); + } + + /* default port if none specified */ + if (portstr == NULL) { + port = (int)URL_DFLT_SRVR_PORT; + } else { + port = (int)strtoul(portstr, &r, 10); + if (*r != '\0') { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_PARSE), uri); + return (OCSPParse); + } + } + + /* allocate new request structure */ + if ((req = OCSP_REQUEST_new()) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_MEM)); + return (OCSPMem); + } + + /* convert cert and issuer fields into OCSP request data */ + if ((id = OCSP_cert_to_id(NULL, cert, issuer)) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_PKG_INTERNAL), + __FILE__, __LINE__); + return (OCSPInternal); + } + + /* fill out request structure with request data */ + if ((OCSP_request_add0_id(req, id)) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_PKG_INTERNAL), + __FILE__, __LINE__); + return (OCSPInternal); + } + + /* add nonce */ + OCSP_request_add1_nonce(req, NULL, -1); + + /* connect to host, or proxy */ + if (proxy != NULL) { + if ((cbio = BIO_new_connect(proxy->hostname)) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_MEM)); + return (OCSPMem); + } + + /* + * BIO_set_conn_int_port takes an int *, so let's give it one + * rather than an ushort_t * + */ + port = proxy->port; + (void) BIO_set_conn_int_port(cbio, &port); + if (BIO_do_connect(cbio) <= 0) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_CONNECT), + proxy->hostname, port); + return (OCSPConnect); + } + } else { + if ((cbio = BIO_new_connect(host)) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_MEM)); + return (OCSPMem); + } + + (void) BIO_set_conn_int_port(cbio, &port); + if (BIO_do_connect(cbio) <= 0) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_CONNECT), + host, port); + return (OCSPConnect); + } + } + + /* calculate length of binary request data */ + len = i2d_OCSP_REQUEST(req, NULL); + + /* send the request headers */ + if (proxy != NULL) { + retval = BIO_printf(cbio, OCSP_REQUEST_FORMAT, uri, len); + } else { + retval = BIO_printf(cbio, OCSP_REQUEST_FORMAT, path, len); + } + + if (retval <= 0) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_SEND), host); + return (OCSPRequest); + } + + /* send the request binary data */ + if (i2d_OCSP_REQUEST_bio(cbio, req) <= 0) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_SEND), host); + return (OCSPRequest); + } + + /* + * read the response into a memory BIO, so we can 'gets' + * (socket bio's don't support BIO_gets) + */ + if ((mem = BIO_new(BIO_s_mem())) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_MEM)); + return (OCSPMem); + } + + while ((len = BIO_read(cbio, ocspbuf, OCSP_BUFSIZ))) { + if (len < 0) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_READ), host); + return (OCSPRequest); + } + if (BIO_write(mem, ocspbuf, len) != len) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_MEM)); + return (OCSPMem); + } + } + + /* now get the first line of the response */ + if (BIO_gets(mem, ocspbuf, OCSP_BUFSIZ) <= 0) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_RESP_PARSE)); + return (OCSPRequest); + } + + /* parse the header response */ + /* it should look like "HTTP/x.x 200 OK" */ + + /* skip past the protocol info */ + for (p = ocspbuf; (*p != '\0') && !isspace(*p); p++) + continue; + + /* skip past whitespace betwen protocol and start of response code */ + while ((*p != '\0') && isspace(*p)) { + p++; + } + + if (*p == '\0') { + /* premature end */ + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_RESP_PARSE), ocspbuf); + return (OCSPRequest); + } + + /* find end of response code */ + for (q = p; (*q != NULL) && !isspace(*q); q++) + continue; + + /* mark end of response code */ + *q++ = '\0'; + + /* parse response code */ + respcode = strtoul(p, &r, 10); + if (*r != '\0') { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_RESP_PARSE), ocspbuf); + return (OCSPRequest); + } + + /* now find beginning of the response string */ + while ((*q != NULL) && isspace(*q)) { + q++; + } + + /* trim whitespace from end of message */ + for (r = (q + strlen(q) - 1); isspace(*r); r--) { + *r = '\0'; + } + + /* response must be OK */ + if (respcode != 200) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_RESP_NOTOK), 200, + respcode, q); + return (OCSPRequest); + } + + /* read headers, looking for content-type or a blank line */ + while (BIO_gets(mem, ocspbuf, OCSP_BUFSIZ) > 0) { + + /* if we get a content type, make sure it's the right type */ + if (ci_strneq(ocspbuf, CONTENT_TYPE_HDR, + strlen(CONTENT_TYPE_HDR))) { + + /* look for the delimiting : */ + p = strchr(ocspbuf + strlen(CONTENT_TYPE_HDR), ':'); + + if (p == NULL) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_RESP_PARSE), ocspbuf); + return (OCSPResponder); + } + + /* skip over ':' */ + p++; + + /* find beginning of the content type */ + while ((*p != NULL) && isspace(*p)) { + p++; + } + + if (!ci_strneq(p, CONTENT_OCSP_RESP, + strlen(CONTENT_OCSP_RESP))) { + /* response is not right type */ + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_RESP_TYPE), + p, CONTENT_OCSP_RESP); + return (OCSPResponder); + } + + /* continue with next header line */ + continue; + } + + /* scan looking for a character */ + for (p = ocspbuf; (*p != '\0') && isspace(*p); p++) { + continue; + } + /* + * if we got to the end of the line with + * no chars, then this is a blank line + */ + if (*p == '\0') { + break; + } + } + + + if (*p != '\0') { + /* last line was not blank */ + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_RESP_PARSE), ocspbuf); + return (OCSPResponder); + } + + /* now read in the binary response */ + if ((resp = d2i_OCSP_RESPONSE_bio(mem, NULL)) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_READ), host); + return (OCSPResponder); + } + + /* free temp BIOs */ + (void) BIO_free(mem); + (void) BIO_free_all(cbio); + cbio = NULL; + + /* make sure request was successful */ + if (OCSP_response_status(resp) != OCSP_RESPONSE_STATUS_SUCCESSFUL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_RESP_NOTOK), + OCSP_RESPONSE_STATUS_SUCCESSFUL, + OCSP_response_status(resp), + OCSP_response_status_str(OCSP_response_status(resp))); + return (OCSPResponder); + } + + /* parse binary response into internal structure */ + if ((bs = OCSP_response_get1_basic(resp)) == NULL) { + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_READ), host); + return (OCSPParse); + } + + /* + * From here to the end of the code, the return values + * should be hard failures + */ + + /* verify the response, warn if no nonce */ + if (OCSP_check_nonce(req, bs) <= 0) { + logerr(pkg_gt(WRN_OCSP_RESP_NONCE)); + } + + if (OCSP_basic_verify(bs, cas, NULL, OCSP_TRUSTOTHER) <= 0) { + while ((errcode = ERR_get_error()) != NULL) { + err_reason = ERR_reason_error_string(errcode); + if (err_reason == NULL) { + err_reason = + gettext(ERR_SIG_INT); + } + pkgerr_add(err, PKGERR_PARSE, (char *)err_reason); + } + pkgerr_add(err, PKGERR_PARSE, gettext(ERR_OCSP_VERIFY_FAIL), + uri); + return (OCSPVerify); + } + + /* check the validity of our certificate */ + if (OCSP_resp_find_status(bs, id, &status, &reason, + &rev, &thisupd, &nextupd) == NULL) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_VERIFY_NO_STATUS), subjname); + return (OCSPVerify); + } + + if ((currtime = time(NULL)) == (time_t)-1) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_VERIFY_NOTIME)); + return (OCSPVerify); + } + + (void) strlcpy(currtimestr, ctime(&currtime), ATTR_MAX); + + /* trim end */ + for (r = currtimestr + strlen(currtimestr) - 1; + isspace(*r); r--) { + *r = '\0'; + } + + if (!OCSP_check_validity(thisupd, nextupd, + OCSP_VALIDITY_PERIOD, -1)) { + if (nextupd != NULL) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_VERIFY_VALIDITY), + get_time_string(thisupd), get_time_string(nextupd), + currtimestr); + } else { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_VERIFY_VALIDITY), + get_time_string(thisupd), + currtimestr); + } + return (OCSPVerify); + } + + if (status != V_OCSP_CERTSTATUS_GOOD) { + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_OCSP_VERIFY_STATUS), subjname, + OCSP_cert_status_str(status)); + return (OCSPVerify); + } + + /* everythign checks out */ + return (OCSPSuccess); +} + +/* + * Name: get_issuer + * Description: Attempts to find the issuing certificate for a given certificate + * This will look in both the list of trusted certificates found in + * the X509_STORE_CTX structure, as well as the list of untrusted + * chain certificates found in the X509_STORE_CTX structure. + * Arguments: + * issuer - The resulting issuer cert is placed here, if found + * ctx - The current verification context + * x - The certificate whose issuer we are looking for + * Returns : Success - The issuer cert was found and placed in *issuer. + * otherwise, appropriate error is returned. + */ +static int +get_issuer(X509 **issuer, X509_STORE_CTX *ctx, X509 *x) +{ + int i, ok; + + /* + * first look in the list of trusted + * certs, using the context's method to do so + */ + if ((ok = ctx->get_issuer(issuer, ctx, x)) > 0) { + return (ok); + } + + if (ctx->untrusted != NULL) { + /* didn't find it in trusted certs, look through untrusted */ + for (i = 0; i < sk_X509_num(ctx->untrusted); i++) { + if (X509_check_issued(sk_X509_value(ctx->untrusted, i), + x) == X509_V_OK) { + *issuer = sk_X509_value(ctx->untrusted, i); + return (1); + } + } + } + *issuer = NULL; + return (0); +} + +/* + * Name: parse_url_proxy + * Description: Parses URL and optional proxy specification, populates static + * 'ps' structure + * + * Arguments: err - where to record any errors. + * url - URL to parse + * proxy - proxy to parse, or NULL for no proxy + * proxy_port - Default proxy port to use if no proxy + * port specified in 'proxy' + * + * Returns : B_TRUE - success, B_FALSE otherwise + * on success, 'ps->url' and 'ps->proxy' are populated + * with parsed data. + */ +static boolean_t +parse_url_proxy(PKG_ERR *err, char *url, char *proxy, ushort_t proxy_port) +{ + boolean_t ret = B_TRUE; + if (!path_valid(url)) { + ret = B_FALSE; + goto cleanup; + } + + if (url_parse(url, &ps->url) != URL_PARSE_SUCCESS) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_PARSE_URL), url); + ret = B_FALSE; + goto cleanup; + } + + if (proxy != NULL) { + if (url_parse_hostport(proxy, &ps->proxy, proxy_port) + != URL_PARSE_SUCCESS) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_BAD_PROXY), proxy); + ret = B_FALSE; + goto cleanup; + } + } + +cleanup: + return (ret); +} + +/* + * Name: web_setup + * Description: Initializes http library settings + * + * Arguments: err - where to record any errors. + * + * Returns : B_TRUE - success, B_FALSE otherwise + */ +static boolean_t +web_setup(PKG_ERR *err) +{ + boolean_t ret = B_TRUE; + static boolean_t keepalive = B_TRUE; + + if ((ps->hps = http_srv_init(&ps->url)) == NULL) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_INIT_SESS), ps->url); + ret = B_FALSE; + goto cleanup; + } + + if (getenv("WEBPKG_DEBUG") != NULL) { + http_set_verbose(B_TRUE); + } + + if (ps->proxy.hostname[0] != '\0' && + http_set_proxy(ps->hps, &ps->proxy) != 0) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_INIT_SESS), ps->url); + ret = B_FALSE; + goto cleanup; + } + if (http_set_keepalive(ps->hps, keepalive) != 0) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_INIT_SESS), ps->url); + ret = B_FALSE; + goto cleanup; + } + if (http_set_socket_read_timeout(ps->hps, ps->timeout) != 0) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_INIT_SESS), ps->url); + ret = B_FALSE; + goto cleanup; + } + if (http_set_random_file(ps->hps, RANDOM) != 0) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_INIT_SESS), ps->url); + ret = B_FALSE; + goto cleanup; + } + + (void) http_set_p12_format(B_TRUE); + +cleanup: + return (ret); +} + +/* + * Name: web_connect + * Description: Makes connection with URL stored in static 'ps' structure. + * + * Arguments: err - where to record any errors. + * + * Returns : WEB_OK - connection successful + * WEB_VERIFY_SETUP - Unable to complete necessary + * SSL setup + * WEB_CONNREFUSED - Connection was refused to web site + * WEB_HOSTDOWN - Host was not responding to request + * WEB_NOCONNECT - Some other connection failure + */ +static WebStatus +web_connect(PKG_ERR *err) +{ + STACK_OF(X509) *sec_cas = NULL; + char *path; + WebStatus ret = WEB_OK; + ulong_t errcode; + uint_t errsrc; + int my_errno = 0; + const char *libhttperr = NULL; + + if (ps->url.https == B_TRUE) { + /* get CA certificates */ + if (find_ca_certs(err, ps->keystore, &sec_cas) != 0) { + ret = WEB_VERIFY_SETUP; + goto cleanup; + } + + if (sk_X509_num(sec_cas) < 1) { + /* no trusted websites */ + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_KEYSTORE_NOTRUST)); + ret = WEB_VERIFY_SETUP; + goto cleanup; + } + + /* + * write out all CA certs to temp file. libwanboot should + * have an interface for giving it a list of trusted certs + * through an in-memory structure, but currently that does + * not exist + */ + if ((path = write_ca_file(err, ps->dwnld_dir, sec_cas, + WEB_CA_PHRASE)) == NULL) { + ret = WEB_VERIFY_SETUP; + goto cleanup; + } + + ps->certfile = path; + if (http_set_password(ps->hps, WEB_CA_PHRASE) != 0) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_HTTPS_PASSWD)); + ret = WEB_VERIFY_SETUP; + goto cleanup; + } + + if (http_set_certificate_authority_file(path) != 0) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_HTTPS_CA)); + ret = WEB_VERIFY_SETUP; + goto cleanup; + } + } + + if (http_srv_connect(ps->hps) != 0) { + while ((errcode = http_get_lasterr(ps->hps, &errsrc)) != 0) { + /* Have an error - is it EINTR? */ + if (errsrc == ERRSRC_SYSTEM) { + my_errno = errcode; + break; + } else if (libhttperr == NULL) { + /* save the first non-system error message */ + libhttperr = http_errorstr(errsrc, errcode); + } + } + switch (my_errno) { + case EINTR: + case ETIMEDOUT: + /* Timed out. Try, try again */ + ret = WEB_TIMEOUT; + break; + case ECONNREFUSED: + ret = WEB_CONNREFUSED; + break; + case EHOSTDOWN: + ret = WEB_HOSTDOWN; + break; + default: + /* some other fatal error */ + ret = WEB_NOCONNECT; + if (libhttperr == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_INIT_CONN), + ps->url.hport.hostname); + } else { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_HTTP), libhttperr); + } + break; + } + } +cleanup: + return (ret); +} + +/* + * Name: write_ca_file + * Description: Writes out a PKCS12 file containing all trusted certs + * found in keystore recorded in static 'ps' structure + * + * This routine is used because the libwanboot library's + * HTTPS routines cannot accept trusted certificates + * through an in-memory structure, when initiating an + * SSL connection. They must be in a PKCS12, which is + * admittedly a poor interface. + * + * Arguments: err - where to record any errors. + * tmpdir - Directory to write certificate file in + * cacerts - Certs to write out + * passwd - password used to encrypt certs + * + * Returns : path to resulting file, if successfullly written, + * otherwise NULL. + */ +static char +*write_ca_file(PKG_ERR *err, char *tmpdir, STACK_OF(X509) *cacerts, + char *passwd) +{ + int fd, len; + FILE *fp; + PKCS12 *p12 = NULL; + char *ret = NULL; + static char tmp_file[PATH_MAX] = ""; + struct stat buf; + + if (!path_valid(tmpdir)) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTEMP), tmpdir); + goto cleanup; + } + + /* mkstemp replaces XXXXXX with a unique string */ + if (((len = snprintf(tmp_file, PATH_MAX, "%s/%sXXXXXX", tmpdir, + "cert")) < 0) || + (len >= PATH_MAX)) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTEMP), tmpdir); + goto cleanup; + } + + if ((fd = mkstemp(tmp_file)) == -1) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTMPFIL), tmp_file); + goto cleanup; + } + + if (fstat(fd, &buf) == -1) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTMPFIL), tmp_file); + goto cleanup; + } + + if (!S_ISREG(buf.st_mode)) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTMPFIL), tmp_file); + goto cleanup; + } + + if ((fp = fdopen(fd, "w")) == NULL) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTMPFIL), tmp_file); + goto cleanup; + } + + if ((p12 = sunw_PKCS12_create(passwd, NULL, NULL, cacerts)) == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_KEYSTORE_FORM), tmp_file); + goto cleanup; + } + + if (i2d_PKCS12_fp(fp, p12) == 0) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_KEYSTORE_FORM), tmp_file); + goto cleanup; + } + + (void) fflush(fp); + (void) fclose(fp); + (void) close(fd); + fp = NULL; + fd = -1; + ret = tmp_file; + +cleanup: + if (p12 != NULL) + PKCS12_free(p12); + if (fp != NULL) + (void) fclose(fp); + if (fd != -1) { + (void) close(fd); + (void) unlink(tmp_file); + } + + return (ret); +} + +/* + * Name: web_send_request + * Description: Sends an HTTP request for a file to the + * web server being communicated with in the static + * 'ps' structure + * + * Arguments: err - where to record any errors. + * request_type - HTTP_REQ_TYPE_HEAD to send an HTTP HEAD request, + * or HTTP_REQ_TYPE_GET to send an HTTP GET request + * cp - + * Returns : WEB_OK - request sent successfully + * WEB_CONNREFUSED - Connection was refused to web site + * WEB_HOSTDOWN - Host was not responding to request + * WEB_NOCONNECT - Some other connection failure + */ +static WebStatus +web_send_request(PKG_ERR *err, int request_type, int cp, int ep) +{ + WebStatus ret = WEB_OK; + ulong_t errcode; + uint_t errsrc; + int my_errno = 0; + const char *libhttperr = NULL; + switch (request_type) { + case HTTP_REQ_TYPE_HEAD: + if ((http_head_request(ps->hps, ps->url.abspath)) != 0) { + while ((errcode = http_get_lasterr(ps->hps, + &errsrc)) != 0) { + /* Have an error - is it EINTR? */ + if (errsrc == ERRSRC_SYSTEM) { + my_errno = errcode; + break; + } else if (libhttperr == NULL) { + /* save first non-system error message */ + libhttperr = + http_errorstr(errsrc, errcode); + } + } + switch (my_errno) { + case EINTR: + case ETIMEDOUT: + /* Timed out. Try, try again */ + ret = WEB_TIMEOUT; + break; + case ECONNREFUSED: + ret = WEB_CONNREFUSED; + break; + case EHOSTDOWN: + ret = WEB_HOSTDOWN; + break; + default: + /* some other fatal error */ + ret = WEB_NOCONNECT; + if (libhttperr == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_INIT_CONN), + ps->url.hport.hostname); + } else { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_HTTP), libhttperr); + } + break; + } + goto cleanup; + } + break; + + case HTTP_REQ_TYPE_GET: + if (cp && ep) { + if (http_get_range_request(ps->hps, ps->url.abspath, + cp, ep - cp) != 0) { + while ((errcode = http_get_lasterr(ps->hps, + &errsrc)) != 0) { + /* Have an error - is it EINTR? */ + if (errsrc == ERRSRC_SYSTEM) { + my_errno = errcode; + break; + } else { + /* + * save first non-system + * error message + */ + libhttperr = + http_errorstr(errsrc, + errcode); + } + } + switch (my_errno) { + case EINTR: + case ETIMEDOUT: + /* Timed out. Try, try again */ + ret = WEB_TIMEOUT; + break; + case ECONNREFUSED: + ret = WEB_CONNREFUSED; + break; + case EHOSTDOWN: + ret = WEB_HOSTDOWN; + break; + default: + /* some other fatal error */ + ret = WEB_NOCONNECT; + if (libhttperr == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_INIT_CONN), + ps->url.hport.hostname); + } else { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_HTTP), + libhttperr); + } + break; + } + goto cleanup; + } + + if (!web_eval_headers(err)) { + ret = WEB_NOCONNECT; + goto cleanup; + } + } else { + if ((http_get_request(ps->hps, ps->url.abspath)) + != 0) { + while ((errcode = http_get_lasterr(ps->hps, + &errsrc)) != 0) { + /* Have an error - is it EINTR? */ + if (errsrc == ERRSRC_SYSTEM) { + my_errno = errcode; + break; + } else { + /* + * save the first non-system + * error message + */ + libhttperr = + http_errorstr(errsrc, + errcode); + } + } + switch (my_errno) { + case EINTR: + case ETIMEDOUT: + /* Timed out. Try, try again */ + ret = WEB_TIMEOUT; + break; + case ECONNREFUSED: + ret = WEB_CONNREFUSED; + break; + case EHOSTDOWN: + ret = WEB_HOSTDOWN; + break; + default: + /* some other fatal error */ + ret = WEB_NOCONNECT; + if (libhttperr == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_INIT_CONN), + ps->url.hport.hostname); + } else { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_HTTP), + libhttperr); + } + break; + } + goto cleanup; + } + + if (!web_eval_headers(err)) { + ret = WEB_NOCONNECT; + goto cleanup; + } + } + break; + default: + pkgerr_add(err, PKGERR_INTERNAL, gettext(ERR_PKG_INTERNAL), + __FILE__, __LINE__); + } + +cleanup: + return (ret); +} + +/* + * Name: web_eval_headers + * Description: Evaluates HTTP headers returned during an HTTP request. + * This must be called before calling + * http_get_header_value(). + * + * Arguments: err - where to record any errors. + * + * Returns : B_TRUE - success, B_FALSE otherwise + */ +static boolean_t +web_eval_headers(PKG_ERR *err) +{ + const char *http_err; + ulong_t herr; + uint_t errsrc; + + if (http_process_headers(ps->hps, &ps->resp) != 0) { + if ((ps->resp != NULL) && (ps->resp->statusmsg != NULL)) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_HTTP), + ps->resp->statusmsg); + } + + herr = http_get_lasterr(ps->hps, &errsrc); + http_err = http_errorstr(errsrc, herr); + pkgerr_add(err, PKGERR_WEB, gettext(ERR_HTTP), + http_err); + return (B_FALSE); + } + return (B_TRUE); +} + +/* + * Name: web_get_file + * Description: Downloads the file URL from the website, all of + * which are recorded in the static 'ps' struct + * + * Arguments: err - where to record any errors. + * dwnld_dir - Directory to download file into + * device - Where to store path to resulting + * file + * nointeract - if non-zero, do not output + * progress + * fname - name of downloaded file link in the dwnld_dir + * + * Returns : WEB_OK - download successful + * WEB_CONNREFUSED - Connection was refused to web site + * WEB_HOSTDOWN - Host was not responding to request + * WEB_GET_FAIL - Unable to initialize download + * state (temp file creation, header parsing, etc) + * WEB_NOCONNECT - Some other connection failure + */ +static WebStatus +web_get_file(PKG_ERR *err, char *dwnld_dir, int nointeract, char **fname) +{ + int i, fd; + int n = 0; + ulong_t abs_pos = 0; + char *head_val = NULL; + char *lastmod_val = NULL; + char *bname = NULL; + struct stat status; + WebStatus ret = WEB_OK; + WebStatus req_ret; + ulong_t errcode; + uint_t errsrc; + int my_errno = 0; + const char *libhttperr = NULL; + char *disp; + char tmp_file[PATH_MAX]; + int len; + + ps->data.prev_cont_length = + ps->data.content_length = + ps->data.cur_pos = 0; + + if ((head_val = http_get_header_value(ps->hps, + CONTENT_LENGTH_HDR)) != NULL) { + ps->data.content_length = atol(head_val); + } else { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_NO_HEAD_VAL), + CONTENT_LENGTH_HDR); + ret = WEB_GET_FAIL; + goto cleanup; + } + + free(head_val); + head_val = NULL; + + if ((head_val = http_get_header_value(ps->hps, + CONTENT_DISPOSITION_HDR)) != NULL) { + /* "inline; parm=val; parm=val */ + if ((disp = strtok(head_val, "; \t\n\f\r")) != NULL) { + /* disp = "inline" */ + while ((disp = strtok(NULL, "; \t\n\f\r")) != NULL) { + /* disp = "parm=val" */ + if (ci_strneq(disp, "filename=", 9)) { + bname = xstrdup(basename(disp + 9)); + trim(bname); + dequote(bname); + } + } + } + free(head_val); + head_val = NULL; + } + + if (bname == NULL) { + /* + * couldn't determine filename from header value, + * so take basename of URL + */ + if ((bname = get_endof_string(ps->url.abspath, '/')) == NULL) { + /* URL is bad */ + pkgerr_add(err, PKGERR_PARSE, + gettext(ERR_PARSE_URL), ps->url.abspath); + ret = WEB_GET_FAIL; + goto cleanup; + } + } + + *fname = bname; + + if ((head_val = http_get_header_value(ps->hps, LAST_MODIFIED_HDR)) + != NULL) { + + if ((lastmod_val = condense_lastmodified(head_val)) == NULL) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_BAD_HEAD_VAL), + LAST_MODIFIED_HDR, head_val); + ret = WEB_GET_FAIL; + goto cleanup; + } + free(head_val); + head_val = NULL; + + if ((ps->uniqfile = get_unique_filename(dwnld_dir, + lastmod_val)) == NULL) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_OPEN_TMP)); + ret = WEB_GET_FAIL; + goto cleanup; + } + + free(lastmod_val); + lastmod_val = NULL; + + if ((fd = open(ps->uniqfile, + O_NONBLOCK|O_RDWR|O_APPEND|O_CREAT|O_EXCL, + 640)) == -1) { + + /* + * A partial downloaded file + * already exists, so open it. + */ + if ((fd = open(ps->uniqfile, + O_NONBLOCK|O_RDWR|O_APPEND)) != -1) { + if (fstat(fd, &status) == -1 || + !S_ISREG(status.st_mode)) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_DWNLD_NO_CONT), + ps->uniqfile); + ret = WEB_GET_FAIL; + goto cleanup; + } else { + echo_out(nointeract, + gettext(MSG_DWNLD_PART), + ps->uniqfile, + status.st_size); + ps->data.prev_cont_length = + status.st_size; + } + } else { + /* unable to open partial file */ + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_DWNLD_NO_CONT), + ps->uniqfile); + ret = WEB_GET_FAIL; + goto cleanup; + } + } + } else { + /* + * no "Last-Modified" header, so this file is not eligible for + * spooling and "resuming last download" operations + */ + ps->spool = B_FALSE; + + /* mkstemp replaces XXXXXX with a unique string */ + if (((len = snprintf(tmp_file, PATH_MAX, + "%s/%sXXXXXX", dwnld_dir, "stream")) < 0) || + (len >= PATH_MAX)) { + pkgerr_add(err, PKGERR_WEB, + gettext(MSG_NOTEMP), dwnld_dir); + ret = WEB_GET_FAIL; + goto cleanup; + } + + if ((fd = mkstemp(tmp_file)) == -1) { + pkgerr_add(err, PKGERR_WEB, + gettext(MSG_NOTMPFIL), tmp_file); + ret = WEB_GET_FAIL; + goto cleanup; + } + + if (fstat(fd, &status) == -1 || + !S_ISREG(status.st_mode)) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_DWNLD_NO_CONT), + ps->uniqfile); + ret = WEB_GET_FAIL; + goto cleanup; + } + + ps->data.prev_cont_length = 0; + ps->uniqfile = xstrdup(tmp_file); + } + + /* File has already been completely downloaded */ + if (ps->data.prev_cont_length == ps->data.content_length) { + echo_out(nointeract, gettext(MSG_DWNLD_PREV), ps->uniqfile); + ps->data.cur_pos = ps->data.prev_cont_length; + if (!make_link(dwnld_dir, bname)) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTEMP), + dwnld_dir); + ret = WEB_GET_FAIL; + goto cleanup; + } + /* we're done, so cleanup and return success */ + goto cleanup; + } else if (ps->data.prev_cont_length != 0) { + ps->data.cur_pos = ps->data.prev_cont_length; + } + + if (!ck_dwnld_dir_space(err, dwnld_dir, + (ps->data.prev_cont_length != 0) ? + (ps->data.content_length - ps->data.cur_pos) : + ps->data.content_length)) { + ret = WEB_GET_FAIL; + goto cleanup; + } + + if ((req_ret = web_send_request(err, HTTP_REQ_TYPE_GET, + ps->data.cur_pos, ps->data.content_length)) != WEB_OK) { + ret = req_ret; + goto cleanup; + } + + if (ps->data.prev_cont_length != 0) + echo_out(nointeract, gettext(MSG_DWNLD_CONT)); + else + echo_out(nointeract, gettext(MSG_DWNLD)); + + progress_setup(nointeract, ps->data.content_length); + + /* Download the file a BLOCK at a time */ + while (ps->data.cur_pos < ps->data.content_length) { + progress_report(nointeract, abs_pos); + i = ((ps->data.content_length - ps->data.cur_pos) < BLOCK) ? + (ps->data.content_length - ps->data.cur_pos) + : BLOCK; + if ((n = http_read_body(ps->hps, ps->content, i)) <= 0) { + while ((errcode = http_get_lasterr(ps->hps, + &errsrc)) != 0) { + /* Have an error - is it EINTR? */ + if (errsrc == ERRSRC_SYSTEM) { + my_errno = errcode; + break; + } else { + /* + * save first non-system + * error message + */ + libhttperr = + http_errorstr(errsrc, errcode); + } + } + switch (my_errno) { + case EINTR: + case ETIMEDOUT: + /* Timed out. Try, try again */ + ret = WEB_TIMEOUT; + break; + case ECONNREFUSED: + ret = WEB_CONNREFUSED; + break; + case EHOSTDOWN: + ret = WEB_HOSTDOWN; + break; + default: + /* some other fatal error */ + ret = WEB_NOCONNECT; + if (libhttperr == NULL) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_INIT_CONN), + ps->url.hport.hostname); + } else { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_HTTP), libhttperr); + } + break; + } + goto cleanup; + } + if ((n = write(fd, ps->content, n)) == 0) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_WRITE), + ps->uniqfile, strerror(errno)); + ret = WEB_GET_FAIL; + goto cleanup; + } + ps->data.cur_pos += n; + abs_pos += n; + } + + progress_finish(nointeract); + echo_out(nointeract, gettext(MSG_DWNLD_COMPLETE)); + + if (!make_link(dwnld_dir, bname)) { + pkgerr_add(err, PKGERR_WEB, gettext(MSG_NOTEMP), + dwnld_dir); + ret = WEB_GET_FAIL; + goto cleanup; + } + +cleanup: + sync(); + if (fd != -1) { + (void) close(fd); + } + + if (head_val != NULL) + free(head_val); + + if (lastmod_val != NULL) + free(lastmod_val); + + return (ret); +} + +/* + * Name: make_link + * Description: Create new link to file being downloaded + * + * Arguments: dwnld_dir - directory in which downloaded file exists + * bname - name of link + * + * Returns : B_TRUE - success, B_FALSE otherwise + */ +static boolean_t +make_link(char *dwnld_dir, char *bname) +{ + int len; + + if ((ps->link = (char *)xmalloc(PATH_MAX)) == NULL) + return (B_FALSE); + if (((len = snprintf(ps->link, PATH_MAX, "%s/%s", + dwnld_dir, bname)) < 0) || + len >= PATH_MAX) + return (B_FALSE); + + (void) link(ps->uniqfile, ps->link); + + return (B_TRUE); +} + +/* + * Name: get_startof_string + * Description: searches string for token, returns a newly-allocated + * substring of the given string up to, but not + * including, token. for example + * get_startof_string("abcd", 'c') will return "ab" + * + * Arguments: path - path to split + * token - character to split on + * + * Returns : substring of 'path', up to, but not including, + * token, if token appears in path. Otherwise, + * returns NULL. + */ +char * +get_startof_string(char *path, char token) +{ + char *p, *p2; + + if (path == NULL) + return (NULL); + + p = xstrdup(path); + + p2 = strchr(p, token); + if (p2 == NULL) { + free(p); + return (NULL); + } else { + *p2 = '\0'; + return (p); + } +} + +/* + * Name: get_endof_string + * Description: searches string for token, returns a + * newly-allocated substring of the given string, + * starting at character following token, to end of + * string. + * + * for example get_end_string("abcd", 'c') + * will return "d" + * + * Arguments: path - path to split + * token - character to split on + * + * Returns : substring of 'path', beginning at character + * following token, to end of string, if + * token appears in path. Otherwise, + * returns NULL. + */ +char * +get_endof_string(char *path, char token) +{ + char *p, *p2; + + if (path == NULL) + return (NULL); + + p = xstrdup(path); + + if ((p2 = strrchr(p, token)) == NULL) { + return (NULL); + } + + return (p2 + 1); +} + +/* + * Name: progress_setup + * Description: Initialize session for reporting progress + * + * Arguments: nointeract - if non-zero, do not do anything + * ulong_t - size of job to report progress for + * + * Returns : none + */ +static void +progress_setup(int nointeract, ulong_t size_of_load) +{ + ulong_t divisor; + ulong_t term_width = TERM_WIDTH; + + if (nointeract) + return; + + if (size_of_load > MED_DWNLD && size_of_load < LARGE_DWNLD) + divisor = MED_DIVISOR; + else if (size_of_load > LARGE_DWNLD) { + term_width = TERM_WIDTH - 8; + divisor = LARGE_DIVISOR; + } else + divisor = SMALL_DIVISOR; + + const_increment = size_of_load / term_width; + const_divider = size_of_load / divisor; + const_completed = 100 / divisor; +} + +/* + * Name: progress_report + * Description: Report progress for current progress context, + * to stderr + * + * Arguments: nointeract - if non-zero, do not do anything + * position - how far along in the job to report. + * This should be <= size used during progress_setup + * + * Returns : none + */ +static void +progress_report(int nointeract, ulong_t position) +{ + static ulong_t increment; + static ulong_t divider; + + if (nointeract) + return; + + if (position == 0) { + increment = const_increment; + divider = const_divider; + } + if (position > increment && position < divider) { + (void) putc('.', stderr); + increment += const_increment; + } else if (position > divider) { + completed += const_completed; + (void) fprintf(stderr, "%ld%c", completed, '%'); + increment += const_increment; + divider += const_divider; + } +} + +/* + * Name: progress_finish + * Description: Finalize session for reporting progress. + * "100%" is reported to screen + * + * Arguments: nointeract - if non-zero, do not do anything + * + * Returns : none + */ +static void +progress_finish(int nointeract) +{ + if (nointeract) + return; + + (void) fprintf(stderr, "%d%c\n", 100, '%'); +} + +/* + * Name: init_session + * Description: Initializes static 'ps' structure with default + * values + * + * Arguments: none + * + * Returns : B_TRUE - success, B_FALSE otherwise + */ +static boolean_t +init_session(void) +{ + if ((ps = (WEB_SESSION *) + xmalloc(sizeof (WEB_SESSION))) == NULL) { + return (B_FALSE); + } + (void) memset(ps, 0, sizeof (*ps)); + + if ((ps->content = (char *)xmalloc(BLOCK)) == NULL) { + return (B_FALSE); + } + + (void) memset(ps->content, 0, BLOCK); + + ps->data.cur_pos = 0UL; + ps->data.content_length = 0UL; + ps->url.https = B_FALSE; + ps->uniqfile = NULL; + ps->link = NULL; + ps->dwnld_dir = NULL; + ps->spool = B_TRUE; + ps->errstr = NULL; + ps->keystore = NULL; + + return (B_TRUE); +} + +/* + * Name: ck_downld_dir_space + * Description: Verify enough space exists in directory to hold file + * + * Arguments: err - where to record any errors. + * dwnld_dir - Directory to check available space in + * bytes_needed - How many bytes are need + * + * Returns : B_TRUE - enough space exists in dwnld_dir to hold + * bytes_needed bytes, otherwise B_FALSE + */ +static boolean_t +ck_dwnld_dir_space(PKG_ERR *err, char *dwnld_dir, ulong_t bytes_needed) +{ + u_longlong_t bytes_avail; + u_longlong_t block_pad; + struct statvfs64 status; + + if (statvfs64(dwnld_dir, &status)) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_TMPDIR), dwnld_dir); + return (B_FALSE); + } + + block_pad = (status.f_frsize ? status.f_frsize : status.f_bsize); + bytes_avail = status.f_bavail * block_pad; + + if ((((u_longlong_t)bytes_needed) + block_pad) > bytes_avail) { + pkgerr_add(err, PKGERR_WEB, gettext(ERR_DISK_SPACE), + dwnld_dir, + (((u_longlong_t)bytes_needed) + block_pad) / 1024ULL, + bytes_avail / 1024ULL); + return (B_FALSE); + } + + return (B_TRUE); +} + +/* + * Description: + * This function returns a unique file name based on the parts of the + * URI. This is done to enable partially downloaded files to be resumed. + * Arguments: + * dir - The directory that should contain the filename. + * last_modified - A string representing the date of last modification, + * used as part of generating unique name + * Returns: + * A valid filename or NULL. + */ + +static char * +get_unique_filename(char *dir, char *last_modified) +{ + char *buf, *buf2, *beg_str; + int len; + + if ((buf = (char *)xmalloc(PATH_MAX)) == NULL) { + return (NULL); + } + if ((buf2 = (char *)xmalloc(PATH_MAX)) == NULL) { + return (NULL); + } + + /* prepare strings for being cat'ed onto */ + buf[0] = buf2[0] = '\0'; + /* + * No validation of the path is done here. We just construct the path + * and it must be validated later + */ + + if (dir) { + if (((len = snprintf(buf2, PATH_MAX, "%s/", dir)) < 0) || + (len >= PATH_MAX)) + return (NULL); + } else { + return (NULL); + } + + if (ps->url.abspath) + if (strlcat(buf, ps->url.abspath, PATH_MAX) >= PATH_MAX) + return (NULL); + if (ps->url.hport.hostname) + if (isdigit((int)ps->url.hport.hostname[0])) { + if (strlcat(buf, ps->url.hport.hostname, PATH_MAX) + >= PATH_MAX) + return (NULL); + } else { + if ((beg_str = + get_startof_string(ps->url.hport.hostname, '.')) + != NULL) + if (strlcat(buf, beg_str, PATH_MAX) >= PATH_MAX) + return (NULL); + } + if (last_modified != NULL) + if (strlcat(buf, last_modified, PATH_MAX) >= PATH_MAX) + return (NULL); + + if ((buf = replace_token(buf, '/', '_')) != NULL) { + if (strlcat(buf2, buf, PATH_MAX) >= PATH_MAX) { + return (NULL); + } else { + if (buf) free(buf); + return (buf2); + } + } else { + if (buf) free(buf); + if (buf2) free(buf2); + return (NULL); + } +} + +/* + * Description: + * Removes token(s) consisting of one character from any path. + * Arguments: + * path - The path to search for the token in. + * token - The token to search for + * Returns: + * The path with all tokens removed or NULL. + */ +static char * +replace_token(char *path, char oldtoken, char newtoken) +{ + char *newpath, *p; + + if ((path == NULL) || (oldtoken == '\0') || (newtoken == '\0')) { + return (NULL); + } + + newpath = xstrdup(path); + + for (p = newpath; *p != '\0'; p++) { + if (*p == oldtoken) { + *p = newtoken; + } + } + + return (newpath); +} + +/* + * Name: trim + * Description: Trims whitespace from a string + * has been registered) + * Scope: private + * Arguments: string - string to trim. It is assumed + * this string is writable up to it's entire + * length. + * Returns: none + */ +static void +trim(char *str) +{ + int len, i; + if (str == NULL) { + return; + } + + len = strlen(str); + /* strip from front */ + while (isspace(*str)) { + for (i = 0; i < len; i++) { + str[i] = str[i+1]; + } + } + + /* strip from back */ + len = strlen(str); + while (isspace(str[len-1])) { + len--; + } + str[len] = '\0'; +} + +/* + * Description: + * Resolves double quotes + * Arguments: + * str - The string to resolve + * Returns: + * None + */ +static void +dequote(char *str) +{ + char *cp; + + if ((str == NULL) || (str[0] != '"')) { + /* no quotes */ + return; + } + + /* remove first quote */ + memmove(str, str + 1, strlen(str) - 1); + + /* + * scan string looking for ending quote. + * escaped quotes like \" don't count + */ + cp = str; + + while (*cp != '\0') { + switch (*cp) { + case '\\': + /* found an escaped character */ + /* make sure end of string is not '\' */ + if (*++cp != '\0') { + cp++; + } + break; + + case '"': + *cp = '\0'; + break; + default: + cp++; + } + } +} + +/* + * Name: get_ENV_proxy + * Description: Retrieves setting of proxy env variable + * + * Arguments: err - where to record any errors. + * proxy - where to store proxy + * + * Returns : B_TRUE - http proxy was found and valid, stored in proxy + * B_FALSE - error, errors recorded in err + */ +static boolean_t +get_ENV_proxy(PKG_ERR *err, char **proxy) +{ + char *buf; + + if ((buf = getenv("HTTPPROXY")) != NULL) { + if (!path_valid(buf)) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_ILL_ENV), "HTTPPROXY", buf); + return (B_FALSE); + } else { + *proxy = buf; + return (B_TRUE); + } + } else { + /* try the other env variable */ + if ((buf = getenv("http_proxy")) != NULL) { + if (!path_valid(buf)) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_ILL_ENV), "http_proxy", buf); + return (B_FALSE); + } + if (!strneq(buf, "http://", 7)) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_ILL_ENV), "http_proxy", buf); + return (B_FALSE); + } + + /* skip over the http:// part of the proxy "url" */ + *proxy = buf + 7; + return (B_TRUE); + } + } + + /* either the env variable(s) were set and valid, or not set */ + return (B_TRUE); +} + +/* + * Name: get_ENV_proxyport + * Description: Retrieves setting of PROXYPORT env variable + * + * Arguments: err - where to record any errors. + * port - where to store resulting port + * + * Returns : B_TRUE - string found in PROXYPORT variable, converted + * to decimal integer, if it exists + * and is valid. Or, PROXYPORT not set, port set to 1. + * B_FALSE - env variable set, but invalid + * (not a number for example) + */ +static boolean_t +get_ENV_proxyport(PKG_ERR *err, ushort_t *port) +{ + char *buf; + ushort_t newport; + buf = getenv("HTTPPROXYPORT"); + if (buf != NULL) { + if (!path_valid(buf)) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_ILL_ENV), "HTTPPROXYPORT", buf); + return (B_FALSE); + } + if ((newport = atoi(buf)) == 0) { + pkgerr_add(err, PKGERR_WEB, + gettext(ERR_ILL_ENV), "HTTPPROXYPORT", buf); + return (B_FALSE); + } + *port = newport; + return (B_TRUE); + } else { + *port = 1; + return (B_TRUE); + } +} + +/* + * Name: remove_dwnld_file + * Description: Removes newly-downloaded file if completely downloaded. + * + * Arguments: path - path to file to remove + * + * Returns : B_TRUE - success, B_FALSE otherwise + * if it's '0' (not OK) we simply return it, since the + * verification operation has already determined that the + * cert is invalid. if 'ok' is non-zero, then we do our + * checks, and return 0 or 1 based on if the cert is + * invalid or valid. + */ +static boolean_t +remove_dwnld_file(char *path) +{ + if (path && path != NULL) { + /* + * Only remove the downloaded file if it has been completely + * downloaded, or is not eligible for spooling + */ + if ((!ps->spool) || + (ps->data.cur_pos >= ps->data.content_length)) { + (void) unlink(path); + } + } else { + return (B_FALSE); + } + return (B_TRUE); +} + +/* + * Name: condense_lastmodifided + * Description: generates a substring of a last-modified string, + * and removes colons. + * + * Arguments: last_modified - string of the form + * "Wed, 23 Oct 2002 21:59:45 GMT" + * + * Returns : + * new string, consisting of hours/minutes/seconds only, + * sans any colons. + */ +char * +condense_lastmodified(char *last_modified) +{ + char *p, *p2; + + /* + * Last-Modified: Wed, 23 Oct 2002 21:59:45 GMT + * Strip the hours, minutes and seconds, without the ':'s, from + * the above string, void of the ':". + */ + + if (last_modified == NULL) + return (NULL); + + if ((p = xstrdup(last_modified)) == NULL) + return (NULL); + p2 = (strstr(p, ":") - 2); + p2[8] = '\0'; + return (replace_token(p2, ':', '_')); +} + +/* + * Name: backoff + * Description: sleeps for a certain # of seconds after a network + * failure. + * Scope: public + * Arguments: none + * Returns: none + */ +void +backoff() +{ + static boolean_t initted = B_FALSE; + int backoff; + long seed; + + if (!initted) { + /* seed the rng */ + (void) _get_random_info(&seed, sizeof (seed)); + srand48(seed); + initted = B_TRUE; + } + + backoff = drand48() * (double)cur_backoff; + (void) sleep(backoff); + if (cur_backoff < MAX_BACKOFF) { + /* + * increase maximum time we might wait + * next time so as to fall off over + * time. + */ + cur_backoff *= BACKOFF_FACTOR; + } +} + +/* + * Name: reset_backoff + * Description: notifies the backoff service that whatever was + * being backoff succeeded. + * Scope: public + * Arguments: none + * Returns: none + */ +void +reset_backoff() +{ + cur_backoff = MIN_BACKOFF; +} + +/* + * Name: _get_random_info + * Description: generate an amount of random bits. Currently + * only a small amount (a long long) can be + * generated at one time. + * Scope: private + * Arguments: buf - [RO, *RW] (char *) + * Buffer to copy bits into + * size - amount to copy + * Returns: B_TRUE on success, B_FALSE otherwise. The buffer is filled + * with the amount of bytes of random data specified. + */ +static boolean_t +_get_random_info(void *buf, int size) +{ + struct timeval tv; + typedef struct { + long low_time; + long hostid; + } randomness; + randomness r; + + /* if the RANDOM file exists, use it */ + if (access(RANDOM, R_OK) == 0) { + if ((RAND_load_file(RANDOM, 1024 * 1024)) > 0) { + if (RAND_bytes((uchar_t *)buf, size) == 1) { + /* success */ + return (B_TRUE); + } + } + } + + /* couldn't use RANDOM file, so fallback to time of day and hostid */ + (void) gettimeofday(&tv, (struct timezone *)0); + + /* Wouldn't it be nice if we could hash these */ + r.low_time = tv.tv_usec; + r.hostid = gethostid(); + + if (sizeof (r) < size) { + /* + * Can't copy correctly + */ + return (B_FALSE); + } + (void) memcpy(buf, &r, size); + return (B_TRUE); +} + +/* + * Name: pkg_passphrase_cb + * Description: Default callback that applications can use when + * a passphrase is needed. This routine collects + * a passphrase from the user using the given + * passphrase retrieval method set with + * set_passphrase_passarg(). If the method + * indicates an interactive prompt, then the + * prompt set with set_passphrase_prompt() + * is displayed. + * + * Arguments: buf - Buffer to copy passphrase into + * size - Max amount to copy to buf + * rw - Whether this passphrase is needed + * to read something off disk, or + * write something to disk. Applications + * typically want to ask twice when getting + * a passphrase for writing something. + * data - application-specific data. In this + * callback, data is a pointer to + * a keystore_passphrase_data structure. + * + * Returns: Length of passphrase collected, or -1 on error. + * Errors recorded in 'err' object in the *data. + */ +int +pkg_passphrase_cb(char *buf, int size, int rw, void *data) +{ + BIO *pwdbio = NULL; + char passphrase_copy[MAX_PHRASELEN + 1]; + PKG_ERR *err; + int passlen; + char *ws; + char prompt_copy[MAX_VERIFY_MSGLEN]; + char *passphrase; + char *arg; + + err = ((keystore_passphrase_data *)data)->err; + + if (passarg == NULL) { + arg = "console"; + } else { + arg = passarg; + } + + /* default method of collecting password is by prompting */ + if (ci_streq(arg, "console")) { + if ((passphrase = getpassphrase(prompt)) == NULL) { + pkgerr_add(err, PKGERR_BADPASS, + gettext(MSG_NOPASS), arg); + return (-1); + } + + if (rw) { + /* + * if the password is being supplied for + * writing something to disk, verify it first + */ + + /* make a copy (getpassphrase overwrites) */ + strlcpy(passphrase_copy, passphrase, + MAX_PHRASELEN + 1); + + if (((passlen = snprintf(prompt_copy, + MAX_VERIFY_MSGLEN, "%s: %s", + gettext(MSG_PASSWD_AGAIN), + prompt)) < 0) || + (passlen >= (MAX_PHRASELEN + 1))) { + pkgerr_add(err, PKGERR_BADPASS, + gettext(MSG_NOPASS), arg); + return (-1); + } + + if ((passphrase = + getpassphrase(prompt_copy)) == NULL) { + pkgerr_add(err, PKGERR_BADPASS, + gettext(MSG_NOPASS), arg); + return (-1); + } + + if (!streq(passphrase_copy, passphrase)) { + pkgerr_add(err, PKGERR_READ, + gettext(MSG_PASSWD_NOMATCH)); + return (-1); + } + } + } else if (ci_strneq(arg, "pass:", 5)) { + passphrase = arg + 5; + } else if (ci_strneq(arg, "env:", 4)) { + passphrase = getenv(arg + 4); + } else if (ci_strneq(arg, "file:", 5)) { + + /* open file for reading */ + if ((pwdbio = BIO_new_file(arg + 5, "r")) == NULL) { + pkgerr_add(err, PKGERR_EXIST, + gettext(MSG_PASSWD_FILE), arg + 5); + return (-1); + } + + /* read first line */ + if (((passlen = BIO_gets(pwdbio, buf, size)) < 1) || + (passlen > size)) { + pkgerr_add(err, PKGERR_READ, gettext(MSG_PASSWD_FILE), + arg + 5); + return (-1); + } + BIO_free_all(pwdbio); + pwdbio = NULL; + + if (passlen == size) { + /* + * password was maximum length, so there is + * no null terminator. null-terminate it + */ + buf[size - 1] = '\0'; + } + + /* first newline found is end of passwd, so nuke it */ + if ((ws = strchr(buf, '\n')) != NULL) { + *ws = '\0'; + } + return (strlen(buf)); + } else { + /* unrecognized passphrase */ + pkgerr_add(err, PKGERR_BADPASS, + gettext(MSG_BADPASSARG), arg); + return (-1); + } + + if (passphrase == NULL) { + /* unable to collect passwd from given source */ + pkgerr_add(err, PKGERR_BADPASS, + gettext(MSG_NOPASS), arg); + return (-1); + } + + strlcpy(buf, passphrase, size); + return (strlen(buf)); +} |