/** * Create an alias list collection. * * @param ib Engine. * @param tx Transaction. * @param name Collection name * @param header Header list to alias * * @returns Status code */ static ib_status_t create_header_alias_list( ib_engine_t *ib, ib_tx_t *tx, const char *name, ib_parsed_header_wrapper_t *header) { ib_field_t *f; ib_list_t *header_list; ib_status_t rc; ib_parsed_name_value_pair_list_t *nvpair; assert(ib != NULL); assert(tx != NULL); assert(name != NULL); assert(header != NULL); /* Create the list */ rc = ib_data_get(tx->data, name, &f); if (rc == IB_ENOENT) { rc = ib_data_add_list(tx->data, name, &f); if (rc != IB_OK) { return rc; } } else if (rc != IB_OK) { return rc; } rc = ib_field_mutable_value(f, ib_ftype_list_mutable_out(&header_list)); if (rc != IB_OK) { return rc; } /* Loop through the list & alias everything */ for(nvpair = header->head; nvpair != NULL; nvpair = nvpair->next) { assert(nvpair); assert(nvpair->value); ib_bytestr_t *bs = NULL; if (ib_bytestr_ptr(nvpair->value) != NULL) { rc = ib_bytestr_alias_mem( &bs, tx->mp, ib_bytestr_ptr(nvpair->value), ib_bytestr_length(nvpair->value) ); } else { rc = ib_bytestr_dup_mem(&bs, tx->mp, (const uint8_t *)"", 0); } if (rc != IB_OK) { ib_log_error_tx( tx, "Error creating bytestring of '%.*s' for %s: %s", (int)ib_bytestr_length(nvpair->name), (const char *)ib_bytestr_ptr(nvpair->name), name, ib_status_to_string(rc) ); return rc; } /* Create a byte string field */ rc = ib_field_create( &f, tx->mp, (const char *)ib_bytestr_const_ptr(nvpair->name), ib_bytestr_length(nvpair->name), IB_FTYPE_BYTESTR, ib_ftype_bytestr_in(bs) ); if (rc != IB_OK) { ib_log_error_tx(tx, "Error creating field of '%.*s' for %s: %s", (int)ib_bytestr_length(nvpair->name), (const char *)ib_bytestr_ptr(nvpair->name), name, ib_status_to_string(rc)); return rc; } /* Add the field to the list */ rc = ib_list_push(header_list, f); if (rc != IB_OK) { ib_log_error_tx(tx, "Error adding alias of '%.*s' to %s list: %s", (int)ib_bytestr_length(nvpair->name), (const char *)ib_bytestr_ptr(nvpair->name), name, ib_status_to_string(rc)); return rc; } } return IB_OK; }
/** * A body filter to intercept response body and feed it to IronBee, * and to buffer the data if required by IronBee configuration. * * @param[in] r The nginx request object. * @param[in] in The data to filter. * @return status propagated from next filter, or OK/Error */ static ngx_int_t ironbee_body_out(ngx_http_request_t *r, ngx_chain_t *in) { ngxib_req_ctx *ctx; ngx_chain_t *link; ib_status_t rc; ib_num_t num; ngx_int_t rv = NGX_OK; ib_txdata_t itxdata; if (r->internal) return ngx_http_next_body_filter(r, in); ctx = ngx_http_get_module_ctx(r, ngx_ironbee_module); assert((ctx != NULL) && (ctx->tx != NULL)); ib_log_debug_tx(ctx->tx, "ironbee_body_out"); if (in == NULL) { /* FIXME: could this happen in circumstances when we should * notify IronBee of end-of-response ? */ ib_log_debug_tx(ctx->tx, "ironbee_body_out: input was null"); cleanup_return ngx_http_next_body_filter(r, in); } ctx = ngx_http_get_module_ctx(r, ngx_ironbee_module); if (ctx->output_filter_done) { ib_log_debug_tx(ctx->tx, "ironbee_body_out: already done"); cleanup_return ngx_http_next_body_filter(r, in); } if (!ctx->output_filter_init) { ctx->output_filter_init = 1; if (ctx->internal_errordoc) { /* If it's our own errordoc, pass it straight through */ /* Should we log anything here? The error will already * have been logged. */ ctx->output_buffering = IOBUF_NOBUF; ctx->response_buf = NULL; ib_log_debug_tx(ctx->tx, "ironbee_body_out: in internal errordoc"); } else { /* Determine whether we're configured to buffer */ rc = ib_context_get(ctx->tx->ctx, "buffer_res", ib_ftype_num_out(&num), NULL); ib_log_debug_tx(ctx->tx, "ironbee_body_out: buffer_res is %d", (int)num); if (rc != IB_OK) ib_log_error_tx(ctx->tx, "Can't determine output buffer configuration!"); if (num == 0) { ib_log_debug_tx(ctx->tx, "ironbee_body_out: NOBUF"); ctx->output_buffering = IOBUF_NOBUF; ctx->response_buf = NULL; } else { /* If we're buffering, initialize the buffer */ ib_log_debug_tx(ctx->tx, "ironbee_body_out: BUFFER"); ctx->output_buffering = IOBUF_BUFFER; } } } ngx_regex_malloc_init(r->pool); for (link = in; link != NULL; link = link->next) { /* Feed the data to ironbee */ itxdata.data = link->buf->pos; itxdata.dlen = link->buf->last - link->buf->pos; ib_log_debug_tx(ctx->tx, "ironbee_body_out: %d bytes", (int)itxdata.dlen); if (itxdata.dlen > 0) { ib_state_notify_response_body_data(ctx->tx->ib, ctx->tx, &itxdata); } /* If IronBee just signaled an error, switch to discard data mode, * and dump anything we already have buffered, */ if ( (STATUS_IS_ERROR(ctx->status)) && !ctx->internal_errordoc && (ctx->output_buffering != IOBUF_DISCARD) ) { ib_log_debug_tx(ctx->tx, "ironbee_body_out: error %d", ctx->status); free_chain(r->pool, ctx->response_buf); ctx->response_buf = NULL; ctx->output_buffering = IOBUF_DISCARD; } else if (ctx->output_buffering == IOBUF_BUFFER) { /* Copy any data to our buffer */ if (ctx->response_buf == NULL) { ctx->response_buf = ngx_pcalloc(r->pool, sizeof(ngx_chain_t)); ctx->response_ptr = ctx->response_buf; } else { ctx->response_ptr->next = ngx_pcalloc(r->pool, sizeof(ngx_chain_t)); ctx->response_ptr = ctx->response_ptr->next; } /* Not sure if any data types need setaside, but let's be safe */ #if NO_COPY_REQUIRED ctx->response_ptr->buf = link->buf; #else if (itxdata.dlen > 0) { ctx->response_ptr->buf = ngx_create_temp_buf(r->pool, itxdata.dlen); memcpy(ctx->response_ptr->buf->pos, link->buf->pos, itxdata.dlen); ctx->response_ptr->buf->last += itxdata.dlen; } else { ctx->response_ptr->buf = ngx_palloc(r->pool, sizeof(ngx_buf_t)); memcpy(ctx->response_ptr->buf, link->buf, sizeof(ngx_buf_t)); } #endif } if (link->buf->last_buf) { ib_log_debug_tx(ctx->tx, "ironbee_body_out: last_buf"); ctx->output_filter_done = 1; } } if (ctx->output_buffering == IOBUF_NOBUF) { /* Normal operation - pass it down the chain */ ib_log_debug_tx(ctx->tx, "ironbee_body_out: passing on"); ctx->start_response = 1; rv = ngx_http_next_body_filter(r, in); } else if (ctx->output_buffering == IOBUF_BUFFER) { ib_log_debug_tx(ctx->tx, "ironbee_body_out: buffering"); if (ctx->output_filter_done) { /* We can pass on the buffered data all at once */ ib_log_debug_tx(ctx->tx, "ironbee_body_out: passing buffer"); ctx->start_response = 1; rv = ngx_http_next_body_filter(r, ctx->response_buf); } } else if (ctx->output_buffering == IOBUF_DISCARD) { ib_log_debug_tx(ctx->tx, "ironbee_body_out: discarding"); if (ctx->output_filter_done) { /* Pass a last bucket with no data */ //ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, // "ironbee_body_out: passing NULL last-buffer"); //ctx->response_buf = ngx_pcalloc(r->pool, sizeof(ngx_chain_t)); //ctx->response_buf->buf = ngx_calloc_buf(r->pool); //ctx->response_buf->buf->last_buf = ctx->response_buf->buf->last_in_chain = 1; //rv = ngx_http_next_body_filter(r, ctx->response_buf); /* FIXME: Is setting rv enough to serve error page */ rv = ctx->status; } } if (ctx->output_filter_done) { ib_log_debug_tx(ctx->tx, "ironbee_body_out: notify_postprocess"); rc = ib_state_notify_postprocess(ctx->tx->ib, ctx->tx); if ((rv == NGX_OK) && (rc != IB_OK)) { rv = NGX_HTTP_INTERNAL_SERVER_ERROR; } rc = ib_state_notify_logging(ctx->tx->ib, ctx->tx); if ((rv == NGX_OK) && (rc != IB_OK)) { rv = NGX_HTTP_INTERNAL_SERVER_ERROR; } } cleanup_return rv; }
/** * A header filter to intercept response line and headers and feed to IronBee. * * @param[in] r The nginx request object. * @return status propagated from next filter, or Error */ static ngx_int_t ironbee_headers_out(ngx_http_request_t *r) { ngxib_req_ctx *ctx; ib_parsed_resp_line_t *rline; ib_parsed_header_wrapper_t *ibhdrs; ib_status_t rc; ngx_list_part_t *part; ngx_table_elt_t *hdr; unsigned int i; char proto[12]; char *status; const char *reason; int status_len, reason_len; /* FIXME: needs more logic here to catch error pages */ if (r->internal) return ngx_http_next_header_filter(r); ctx = ngx_http_get_module_ctx(r, ngx_ironbee_module); assert((ctx != NULL) && (ctx->tx != NULL)); ngx_regex_malloc_init(r->pool); /* Notify IronBee of request line and headers */ sprintf(proto, "HTTP/%d.%d", r->http_major, r->http_minor); if (r->headers_out.status_line.len) { status = (char*)r->headers_out.status_line.data; status_len = strcspn(status, " \t"); for (reason = status+status_len; isspace(*reason); ++reason); reason_len = r->headers_out.status_line.len - (reason-status); } else if (r->headers_out.status >= 100 && r->headers_out.status < 600) { status = ngx_palloc(r->pool, 4); /* cast to int, because ngx_int_t requires different format args * on different platforms. We're already limited to 3-digit numbers. */ sprintf(status, "%d", (int)r->headers_out.status); status_len = 3; reason = ""; reason_len = 0; } else { ib_log_error_tx(ctx->tx, "IronBee: bogus response status %d", (int)r->headers_out.status); cleanup_return NGX_ERROR; } rc = ib_parsed_resp_line_create(ctx->tx, &rline, NULL, 0, proto, strlen(proto), status, status_len, reason, reason_len); if (rc != IB_OK) cleanup_return NGX_ERROR; ib_state_notify_response_started(ctx->tx->ib, ctx->tx, rline); rc = ib_parsed_name_value_pair_list_wrapper_create(&ibhdrs, ctx->tx); if (rc != IB_OK) cleanup_return NGX_ERROR; for (part = &r->headers_out.headers.part; part != NULL; part = part->next) { hdr = part->elts; for (i = 0; i < part->nelts; ++i) { ib_parsed_name_value_pair_list_add(ibhdrs, (const char*)hdr->key.data, hdr->key.len, (const char*)hdr->value.data, hdr->value.len); ++hdr; } } /* IronBee currently crashes if called here with no headers, * even perfectly correctly on a 204/304 response. */ if (ibhdrs->size > 0) { rc = ib_state_notify_response_header_data(ctx->tx->ib, ctx->tx, ibhdrs); if (rc != IB_OK) cleanup_return NGX_ERROR; } rc = ib_state_notify_response_header_finished(ctx->tx->ib, ctx->tx); if (rc != IB_OK) cleanup_return NGX_ERROR; ctx->hdrs_out = 1; cleanup_return ngx_http_next_header_filter(r); }
/** * Handler function to generate an error response */ static void error_response(TSHttpTxn txnp, tsib_txn_ctx *txndata) { const char *reason; TSMBuffer bufp; TSMLoc hdr_loc; TSMLoc field_loc; hdr_list *hdrs; TSReturnCode rv; /* make caller responsible for sanity checking */ assert((txndata != NULL) && (txndata->tx != NULL)); reason = TSHttpHdrReasonLookup(txndata->status); if (TSHttpTxnClientRespGet(txnp, &bufp, &hdr_loc) != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: couldn't retrieve client response header."); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); return; } rv = TSHttpHdrStatusSet(bufp, hdr_loc, txndata->status); if (rv != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: TSHttpHdrStatusSet"); } if (reason == NULL) { reason = "Other"; } rv = TSHttpHdrReasonSet(bufp, hdr_loc, reason, strlen(reason)); if (rv != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: TSHttpHdrReasonSet"); } while (hdrs = txndata->err_hdrs, hdrs != 0) { txndata->err_hdrs = hdrs->next; rv = TSMimeHdrFieldCreate(bufp, hdr_loc, &field_loc); if (rv != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: TSMimeHdrFieldCreate"); continue; } rv = TSMimeHdrFieldNameSet(bufp, hdr_loc, field_loc, hdrs->hdr, strlen(hdrs->hdr)); if (rv != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: TSMimeHdrFieldNameSet"); goto errordoc_free1; } rv = TSMimeHdrFieldValueStringInsert(bufp, hdr_loc, field_loc, -1, hdrs->value, strlen(hdrs->value)); if (rv != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: TSMimeHdrFieldValueStringInsert"); goto errordoc_free1; } rv = TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc); if (rv != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: TSMimeHdrFieldAppend"); goto errordoc_free1; } errordoc_free1: rv = TSHandleMLocRelease(bufp, hdr_loc, field_loc); if (rv != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: TSHandleMLocRelease 1"); continue; } } if (txndata->err_body) { /* this will free the body, so copy it first! */ TSHttpTxnErrorBodySet(txnp, txndata->err_body, txndata->err_body_len, NULL); } rv = TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); if (rv != TS_SUCCESS) { ib_log_error_tx(txndata->tx, "ErrorDoc: TSHandleMLocRelease 2"); } ib_log_debug_tx(txndata->tx, "Sent error page %d \"%s\".", txndata->status, reason); }
/** * Plugin for the IronBee ATS. * * Handles some ATS events. * * @param[in,out] contp Pointer to the continuation * @param[in,out] event Event from ATS * @param[in,out] edata Event data * * @returns status */ int ironbee_plugin(TSCont contp, TSEvent event, void *edata) { ib_status_t rc; TSCont mycont; TSHttpTxn txnp = (TSHttpTxn) edata; TSHttpSsn ssnp = (TSHttpSsn) edata; tsib_txn_ctx *txndata; tsib_ssn_ctx *ssndata; tsib_hdr_outcome status; TSMutex ts_mutex = NULL; TSDebug("ironbee", "Entering ironbee_plugin with %d", event); switch (event) { /* CONNECTION */ case TS_EVENT_HTTP_SSN_START: /* start of connection */ /* But we can't initialize conn stuff here, because there's * no API to get the connection stuff required by ironbee * at this point. So instead, intercept the first TXN * * what we can and must do: create a new contp whose * lifetime is our ssn */ ts_mutex = TSMutexCreate(); mycont = TSContCreate(ironbee_plugin, ts_mutex); TSHttpSsnHookAdd (ssnp, TS_HTTP_TXN_START_HOOK, mycont); ssndata = TSmalloc(sizeof(*ssndata)); memset(ssndata, 0, sizeof(*ssndata)); /* The only failure here is EALLOC, and if that happens * we're ****ed anyway */ rc = ib_lock_create_malloc(&(ssndata->mutex)); assert(rc == IB_OK); ssndata->contp = mycont; ssndata->ts_mutex = ts_mutex; TSContDataSet(mycont, ssndata); TSHttpSsnHookAdd (ssnp, TS_HTTP_SSN_CLOSE_HOOK, mycont); TSHttpSsnReenable (ssnp, TS_EVENT_HTTP_CONTINUE); break; case TS_EVENT_HTTP_TXN_START: { /* start of Request */ /* First req on a connection, we set up conn stuff */ ib_status_t rc; ib_engine_t *ib = NULL; ssndata = TSContDataGet(contp); ib_lock_lock(ssndata->mutex); if (ssndata->iconn == NULL) { rc = tsib_manager_engine_acquire(&ib); if (rc == IB_DECLINED) { /* OK, this means the manager is disabled deliberately, * but otherwise all's well. So this TXN * gets processed without intervention from Ironbee * and is invisble when our SSN_CLOSE hook runs. */ ib_lock_unlock(ssndata->mutex); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); TSDebug("ironbee", "Decline from engine manager"); break; } else if (rc != IB_OK) { TSError("[ironbee] Failed to acquire engine: %s", ib_status_to_string(rc)); goto noib_error; } if (ib != NULL) { rc = ib_conn_create(ib, &ssndata->iconn, contp); if (rc != IB_OK) { TSError("[ironbee] ib_conn_create: %s", ib_status_to_string(rc)); tsib_manager_engine_release(ib); goto noib_error; } /* In the normal case, release the engine when the * connection's memory pool is destroyed */ rc = ib_mm_register_cleanup(ssndata->iconn->mm, cleanup_ib_connection, ib); if (rc != IB_OK) { TSError("[ironbee] ib_mm_register_cleanup: %s", ib_status_to_string(rc)); tsib_manager_engine_release(ib); goto noib_error; } TSDebug("ironbee", "CONN CREATE: conn=%p", ssndata->iconn); ssndata->txnp = txnp; ssndata->txn_count = ssndata->closing = 0; rc = ironbee_conn_init(ssndata); if (rc != IB_OK) { TSError("[ironbee] ironbee_conn_init: %s", ib_status_to_string(rc)); goto noib_error; } TSContDataSet(contp, ssndata); TSDebug("ironbee", "ironbee_plugin: ib_state_notify_conn_opened()"); rc = ib_state_notify_conn_opened(ib, ssndata->iconn); if (rc != IB_OK) { TSError("[ironbee] Failed to notify connection opened: %s", ib_status_to_string(rc)); } } else { /* Use TSError where there's no ib or tx */ TSError("Ironbee: No ironbee engine!"); goto noib_error; } } /* create a txn cont (request ctx) and tx */ txndata = TSmalloc(sizeof(*txndata)); memset(txndata, 0, sizeof(*txndata)); txndata->ssn = ssndata; txndata->txnp = txnp; rc = ib_tx_create(&txndata->tx, ssndata->iconn, txndata); if (rc != IB_OK) { TSError("[ironbee] Failed to create tx: %d", rc); tsib_manager_engine_release(ib); TSfree(txndata); goto noib_error; } ++ssndata->txn_count; ib_lock_unlock(ssndata->mutex); ib_log_debug_tx(txndata->tx, "TX CREATE: conn=%p tx=%p id=%s txn_count=%d", ssndata->iconn, txndata->tx, txndata->tx->id, txndata->ssn->txn_count); mycont = TSContCreate(ironbee_plugin, ssndata->ts_mutex); TSContDataSet(mycont, txndata); TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, mycont); /* Hook to process responses */ TSHttpTxnHookAdd(txnp, TS_HTTP_READ_RESPONSE_HDR_HOOK, mycont); /* Hook to process requests */ TSHttpTxnHookAdd(txnp, TS_HTTP_READ_REQUEST_HDR_HOOK, mycont); /* Create continuations for input and output filtering * to give them txn lifetime. */ txndata->in_data_cont = TSTransformCreate(in_data_event, txnp); TSContDataSet(txndata->in_data_cont, txndata); txndata->out_data_cont = TSTransformCreate(out_data_event, txnp); TSContDataSet(txndata->out_data_cont, txndata); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); break; noib_error: ib_lock_unlock(ssndata->mutex); /* NULL txndata signals this to SEND_RESPONSE */ mycont = TSContCreate(ironbee_plugin, ssndata->ts_mutex); TSContDataSet(mycont, NULL); TSError("[ironbee] Internal error initialising for transaction"); TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, mycont); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR); break; } /* HTTP RESPONSE */ case TS_EVENT_HTTP_READ_RESPONSE_HDR: txndata = TSContDataGet(contp); if (txndata->tx == NULL) { TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); break; } /* Feed ironbee the headers if not done already. */ if (!ib_flags_all(txndata->tx->flags, IB_TX_FRES_STARTED)) { status = process_hdr(txndata, txnp, &tsib_direction_server_resp); /* OK, if this was an HTTP 100 response, it's not the * response we're interested in. No headers have been * sent yet, and no data will be sent until we've * reached here again with the final response. */ if (status == HDR_HTTP_100) { TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); break; } // FIXME: Need to know if this fails as it (I think) means // that the response did not come from the server and // that ironbee should ignore it. /* I've not seen a fail here. AFAICT if either the origin * isn't responding or we're responding from cache. we * never reach here in the first place. */ } /* If ironbee signalled an error while processing request body data, * this is the first opportunity to divert to an errordoc */ if (HTTP_CODE(txndata->status)) { ib_log_debug_tx(txndata->tx, "HTTP code %d contp=%p", txndata->status, contp); TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR); break; } /* If we're not going to inspect response body data * we can bring forward notification of response-end * so we're in time to respond with an errordoc if Ironbee * wants to block in the response phase. * * This currently fails. However, that appears to be because I * can't unset IB_TX_FINSPECT_RESBODY with InspectionEngineOptions */ if (!ib_flags_all(txndata->tx->flags, IB_TX_FINSPECT_RESBODY)) { if (!ib_flags_all(txndata->tx->flags, IB_TX_FRES_STARTED) ) { ib_state_notify_response_started(txndata->tx->ib, txndata->tx, NULL); } if (!ib_flags_all(txndata->tx->flags, IB_TX_FRES_FINISHED) ) { ib_state_notify_response_finished(txndata->tx->ib, txndata->tx); } /* Test again for Ironbee telling us to block */ if (HTTP_CODE(txndata->status)) { TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR); break; } } /* Flag that we're too late to divert to an error response */ ib_tx_flags_set(txndata->tx, IB_TX_FCLIENTRES_STARTED); /* Normal execution. Add output filter to inspect response. */ TSHttpTxnHookAdd(txnp, TS_HTTP_RESPONSE_TRANSFORM_HOOK, txndata->out_data_cont); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); break; /* Hook for processing response headers. */ case TS_EVENT_HTTP_SEND_RESPONSE_HDR: txndata = TSContDataGet(contp); if (txndata == NULL) { /* Ironbee is unavailable to help with our response. */ internal_error_response(txnp); /* This contp isn't going through the normal flow. */ TSContDestroy(contp); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); break; } /* If ironbee has sent us into an error response then * we came here in our error path, with nonzero status. */ if (txndata->status != 0) { error_response(txnp, txndata); } /* Feed ironbee the headers if not done already. */ if (!ib_flags_all(txndata->tx->flags, IB_TX_FRES_STARTED)) { if (process_hdr(txndata, txnp, &tsib_direction_client_resp) != HDR_OK) { /* I think this is a shouldn't happen event, and that * if it does we have an ironbee bug or misconfiguration. * Log an error to catch if it happens in practice. */ ib_log_error_tx(txndata->tx, "process_hdr returned error in send_response_hdr event"); } } /* If there is an ironbee-generated response body, notify ironbee. * * NOTE: I do not see anywhere else to put this as the error body is * just a buffer and not delivered via normal IO channels, so * the error body will never get caught by an event. */ if ((txndata->status != 0) && (txndata->err_body != NULL)) { const char *data = txndata->err_body; size_t data_length = txndata->err_body_len; ib_log_debug_tx(txndata->tx, "error_response: calling ib_state_notify_response_body_data() %s:%d", __FILE__, __LINE__); ib_state_notify_response_body_data(txndata->tx->ib, txndata->tx, data, data_length); } TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); break; /* HTTP REQUEST */ case TS_EVENT_HTTP_READ_REQUEST_HDR: /* hook to examine output headers. They're not available yet */ TSHttpTxnHookAdd(txnp, TS_HTTP_PRE_REMAP_HOOK, contp); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); break; /* hook for processing incoming request/headers * The OS_DNS hook is an alternative here. */ case TS_EVENT_HTTP_PRE_REMAP: { int request_inspection_finished = 0; txndata = TSContDataGet(contp); assert ((txndata != NULL) && (txndata->tx != NULL)); status = process_hdr(txndata, txnp, &tsib_direction_client_req); if (HDR_OUTCOME_IS_HTTP_OR_ERROR(status, txndata)) { if (status == HDR_HTTP_STATUS) { ib_log_debug_tx(txndata->tx, "HTTP code %d contp=%p", txndata->status, contp); } else { /* Ironbee set a status we don't handle. * We returned EINVAL, but we also need housekeeping to * avoid a crash in modhtp and log something bad. */ ib_log_debug_tx(txndata->tx, "Internal error %d contp=%p", txndata->status, contp); /* Ugly hack: notifications to stop modhtp bombing out */ request_inspection_finished = 1; } } else { /* Other nonzero statuses not supported */ switch(status) { case HDR_OK: /* If we're not inspecting the Request body, * we can bring forward notification of end-request * so any header-only tests run on Request phase * can abort the tx before opening a backend connection. */ if (!ib_flags_all(txndata->tx->flags, IB_TX_FINSPECT_REQBODY)) { request_inspection_finished = 1; } break; /* All's well */ case HDR_HTTP_STATUS: // FIXME: should we take the initiative here and return 500? ib_log_error_tx(txndata->tx, "Internal error: ts-ironbee requested error but no error response set."); break; case HDR_HTTP_100: /* This can't actually happen with current Trafficserver * versions, as TS will generate a 400 error without * reference to us. But in case that changes in future ... */ ib_log_error_tx(txndata->tx, "No request headers found."); break; default: ib_log_error_tx(txndata->tx, "Unhandled state arose in handling request headers."); break; } } if (request_inspection_finished) { if (!ib_flags_all(txndata->tx->flags, IB_TX_FREQ_STARTED) ) { ib_state_notify_request_started(txndata->tx->ib, txndata->tx, NULL); } if (!ib_flags_all(txndata->tx->flags, IB_TX_FREQ_FINISHED) ) { ib_state_notify_request_finished(txndata->tx->ib, txndata->tx); } } else { /* hook an input filter to watch data */ TSHttpTxnHookAdd(txnp, TS_HTTP_REQUEST_TRANSFORM_HOOK, txndata->in_data_cont); } /* Flag that we can no longer prevent a request going to backend */ ib_tx_flags_set(txndata->tx, IB_TX_FSERVERREQ_STARTED); /* Check whether Ironbee told us to block the request. * This could now come not just from process_hdr, but also * from a brought-forward notification if we aren't inspecting * a request body and notified request_finished. */ if (HTTP_CODE(txndata->status)) { TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR); } else { TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); } break; } /* CLEANUP EVENTS */ case TS_EVENT_HTTP_TXN_CLOSE: { txndata = TSContDataGet(contp); TSContDestroy(txndata->out_data_cont); TSContDestroy(txndata->in_data_cont); TSContDataSet(contp, NULL); TSContDestroy(contp); if ( (txndata != NULL) && (txndata->tx != NULL) ) { ib_log_debug_tx(txndata->tx, "TXN Close: %p", (void *)contp); tsib_txn_ctx_destroy(txndata); } TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); break; } case TS_EVENT_HTTP_SSN_CLOSE: TSDebug("ironbee", "SSN Close: %p", (void *)contp); tsib_ssn_ctx_destroy(TSContDataGet(contp)); tsib_manager_engine_cleanup(); TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE); break; case TS_EVENT_MGMT_UPDATE: { TSDebug("ironbee", "Management update"); ib_status_t rc; rc = tsib_manager_engine_create(); if (rc != IB_OK) { TSError("[ironbee] Error creating new engine: %s", ib_status_to_string(rc)); } break; } /* if we get here we've got a bug */ default: TSError("[ironbee] *** Unhandled event %d in ironbee_plugin.", event); break; } return 0; }
/** * Set the matches into the given field name as .0, .1, .2 ... .9. * * @param[in] tx Current transaction. * @param[in] capture Collection to capture to. * @param[in] ovector The vector of integer pairs of matches from PCRE. * @param[in] matches The number of matches. * @param[in] subject The matched-against string data. * * @returns IB_OK or IB_EALLOC. */ static ib_status_t pcre_set_matches( const ib_tx_t *tx, ib_field_t *capture, int *ovector, int matches, const char *subject ) { assert(tx != NULL); assert(tx->ib != NULL); assert(capture != NULL); assert(ovector != NULL); ib_status_t rc; int i; rc = ib_capture_clear(capture); if (rc != IB_OK) { ib_log_error_tx(tx, "Error clearing captures: %s", ib_status_to_string(rc)); } /* We have a match! Now populate TX:0-9 in tx->data. */ ib_log_debug2_tx(tx, "REGEX populating %d matches", matches); for (i = 0; i < matches; ++i) { /* The length of the match. */ size_t match_len; /* The first character in the match. */ const char *match_start; /* Field name */ const char *name; /* Holder for a copy of the field value when creating a new field. */ ib_bytestr_t *bs; /* Field holder. */ ib_field_t *field; /* Readability. Mark the start and length of the string. */ match_start = subject+ovector[i*2]; match_len = ovector[i*2+1] - ovector[i*2]; /* Create a byte-string representation */ rc = ib_bytestr_dup_mem(&bs, tx->mp, (const uint8_t*)match_start, match_len); if (rc != IB_OK) { return rc; } /* Create a field to hold the byte-string */ name = ib_capture_name(i); rc = ib_field_create(&field, tx->mp, name, strlen(name), IB_FTYPE_BYTESTR, ib_ftype_bytestr_in(bs)); if (rc != IB_OK) { return rc; } /* Add it to the capture collection */ rc = ib_capture_set_item(capture, i, tx->mp, field); if (rc != IB_OK) { return rc; } } return IB_OK; }
/** * @brief Execute the dfa operator * * @param[in] tx Current transaction. * @param[in] instance_data Instance data needed for execution. * @param[in] field The field to operate on. * @param[in] capture If non-NULL, the collection to capture to. * @param[out] result The result of the operator 1=true 0=false. * @param[in] cbdata Callback data. * * @returns IB_OK most times. IB_EALLOC when a memory allocation error handles. */ static ib_status_t dfa_operator_execute( ib_tx_t *tx, void *instance_data, const ib_field_t *field, ib_field_t *capture, ib_num_t *result, void *cbdata ) { assert(instance_data != NULL); assert(tx != NULL); int matches; ib_status_t ib_rc; const int ovecsize = 3 * MATCH_MAX; modpcre_operator_data_t *operator_data = (modpcre_operator_data_t *)instance_data; int *ovector; const char *subject; size_t subject_len; const ib_bytestr_t *bytestr; dfa_workspace_t *dfa_workspace; const char *id = operator_data->id; int options; /* dfa exec options. */ int start_offset; int match_count; const ib_module_t *m = (const ib_module_t *)cbdata; assert(m != NULL); assert(operator_data->cpdata->is_dfa == true); ovector = (int *)malloc(ovecsize*sizeof(*ovector)); if (ovector==NULL) { return IB_EALLOC; } if (field->type == IB_FTYPE_NULSTR) { ib_rc = ib_field_value(field, ib_ftype_nulstr_out(&subject)); if (ib_rc != IB_OK) { free(ovector); return ib_rc; } subject_len = strlen(subject); } else if (field->type == IB_FTYPE_BYTESTR) { ib_rc = ib_field_value(field, ib_ftype_bytestr_out(&bytestr)); if (ib_rc != IB_OK) { free(ovector); return ib_rc; } subject_len = ib_bytestr_length(bytestr); subject = (const char *) ib_bytestr_const_ptr(bytestr); } else { free(ovector); return IB_EINVAL; } /* Get the per-tx workspace data for this rule data id. */ ib_rc = get_dfa_tx_data(m, tx, id, &dfa_workspace); if (ib_rc == IB_ENOENT) { /* First time we are called, clear the captures. */ if (capture) { ib_rc = ib_capture_clear(capture); if (ib_rc != IB_OK) { ib_log_error_tx(tx, "Error clearing captures: %s", ib_status_to_string(ib_rc)); } } options = PCRE_PARTIAL_SOFT; ib_rc = alloc_dfa_tx_data(m, tx, operator_data->cpdata, id, &dfa_workspace); if (ib_rc != IB_OK) { free(ovector); return ib_rc; } } else if (ib_rc == IB_OK) { options = PCRE_PARTIAL_SOFT | PCRE_DFA_RESTART; } else { free(ovector); return ib_rc; } /* Perform the match. * If capturing is specified, then find all matches. */ start_offset = 0; match_count = 0; do { matches = pcre_dfa_exec(operator_data->cpdata->cpatt, operator_data->cpdata->edata, subject, subject_len, start_offset, /* Starting offset. */ options, ovector, ovecsize, dfa_workspace->workspace, dfa_workspace->wscount); if (matches > 0) { ++match_count; /* Use the longest match - the first in ovector - * to set the offset in the subject for the next * match. */ start_offset = ovector[1] + 1; if (capture) { pcre_dfa_set_match(tx, capture, ovector, 1, subject); } } } while (capture && (matches > 0)); if (match_count > 0) { ib_rc = IB_OK; *result = 1; } else if ((matches == 0) || (matches == PCRE_ERROR_NOMATCH)) { ib_rc = IB_OK; *result = 0; } else if (matches == PCRE_ERROR_PARTIAL) { ib_rc = IB_OK; *result = 0; } else { /* Some other error occurred. Set the status to false and * return the error. */ ib_rc = IB_EUNKNOWN; *result = 0; } free(ovector); return ib_rc; }
/** * Handle request_header events for remote IP extraction. * * Extract the "request_headers" field (a list) from the transactions's * data provider instance, then loop through the list, looking for the * "X-Forwarded-For" field. If found, the first value in the (comma * separated) list replaces the local ip address string in the connection * object. * * @param[in] ib IronBee object * @param[in,out] tx Transaction object * @param[in] event Event type * @param[in] cbdata Callback data (module) * * @returns Status code */ static ib_status_t modua_remoteip(ib_engine_t *ib, ib_tx_t *tx, ib_state_event_type_t event, void *cbdata) { assert(ib != NULL); assert(tx != NULL); assert(tx->var_store != NULL); assert(event == handle_context_tx_event); const ib_module_t *m = (const ib_module_t *)cbdata; ib_field_t *field = NULL; ib_status_t rc = IB_OK; const ib_bytestr_t *bs; const uint8_t *data; size_t len; char *buf; uint8_t *comma; const ib_list_t *list; const ib_list_node_t *node; const ib_field_t *forwarded; uint8_t *stripped; size_t num; ib_flags_t flags; const modua_config_t *cfg; rc = ib_context_module_config(ib_context_main(ib), m, &cfg); if (rc != IB_OK) { ib_log_error_tx(tx, "Can't fetch configuration: %s", ib_status_to_string(rc)); return rc; } ib_log_debug3_tx(tx, "Checking for alternate remote address"); /* Extract the X-Forwarded-For header field */ rc = ib_var_target_get_const( cfg->forwarded_for, &list, tx->mp, tx->var_store ); if (rc == IB_ENOENT || ib_list_elements(list) == 0) { ib_log_debug_tx(tx, "No X-Forwarded-For"); return IB_OK; } if (rc != IB_OK) { ib_log_error_tx(tx, "Cannot retrieve request_headers:User-Agent: %d", rc); return rc; } num = ib_list_elements(list); if (num == 0) { ib_log_debug_tx(tx, "No X-Forwarded-For header found"); return rc; } else if (num != 1) { ib_log_debug_tx(tx, "%zd X-Forwarded-For headers found: ignoring", num); return rc; } node = ib_list_last_const(list); if ( (node == NULL) || (node->data == NULL) ) { ib_log_notice_tx(tx, "Invalid X-Forwarded-For header found"); return rc; } forwarded = (const ib_field_t *)node->data; /* Found it: copy the data into a newly allocated string buffer */ rc = ib_field_value_type(forwarded, ib_ftype_bytestr_out(&bs), IB_FTYPE_BYTESTR); if (rc != IB_OK) { ib_log_notice_tx(tx, "Invalid X-Forwarded-For header value"); return rc; } if (bs == NULL) { ib_log_notice_tx(tx, "X-Forwarded-For header not a bytestr"); return IB_EINVAL; } len = ib_bytestr_length(bs); data = ib_bytestr_const_ptr(bs); /* Search for a comma in the buffer */ comma = memchr(data, ',', len); if (comma != NULL) { len = comma - data; } /* Trim whitespace */ stripped = (uint8_t *)data; rc = ib_strtrim_lr_ex(IB_STROP_INPLACE, tx->mp, stripped, len, &stripped, &len, &flags); if (rc != IB_OK) { return rc; } /* Verify that it looks like a valid IP v4/6 address */ rc = ib_ip_validate_ex((const char *)stripped, len); if (rc != IB_OK) { ib_log_error_tx(tx, "X-Forwarded-For \"%.*s\" is not a valid IP address", (int)len, stripped ); return IB_OK; } /* Allocate memory for copy of stripped string */ buf = (char *)ib_mpool_alloc(tx->mp, len+1); if (buf == NULL) { ib_log_error_tx(tx, "Failed to allocate %zd bytes for remote address", len+1); return IB_EALLOC; } /* Copy the string out */ memcpy(buf, stripped, len); buf[len] = '\0'; ib_log_debug_tx(tx, "Remote address changed to \"%s\"", buf); /* This will lose the pointer to the original address * buffer, but it should be cleaned up with the rest * of the memory pool. */ tx->er_ipstr = buf; /* Update the remote address field in the tx collection */ rc = ib_field_create_bytestr_alias( &field, tx->mp, "", 0, (uint8_t *)buf, len ); if (rc != IB_OK) { ib_log_error_tx(tx, "Failed to create field for remote_addr: %s", ib_status_to_string(rc)); return rc; } rc = ib_var_source_set(cfg->remote_addr, tx->var_store, field); if (rc != IB_OK) { ib_log_error_tx(tx, "Failed to set remote address var: %s", ib_status_to_string(rc)); return rc; } return IB_OK; }
/** * Handle request_header events for user agent extraction. * * Extract the "request_headers" field (a list) from the transactions's * data provider instance, then loop through the list, looking for the * "User-Agent" field. If found, the value is parsed and used to update the * connection object fields. * * @param[in] ib IronBee object * @param[in,out] tx Transaction. * @param[in] event Event type * @param[in] data Callback data (module) * * @returns Status code */ static ib_status_t modua_user_agent(ib_engine_t *ib, ib_tx_t *tx, ib_state_event_type_t event, void *data) { assert(ib != NULL); assert(tx != NULL); assert(tx->var_store != NULL); assert(event == handle_context_tx_event); assert(data != NULL); const ib_module_t *m = (const ib_module_t *)data; const ib_field_t *req_agent = NULL; ib_status_t rc = IB_OK; const ib_list_t *bs_list; const ib_bytestr_t *bs; const modua_config_t *cfg; rc = ib_context_module_config(ib_context_main(ib), m, &cfg); if (rc != IB_OK) { ib_log_error_tx(tx, "Can't fetch configuration: %s", ib_status_to_string(rc)); return rc; } /* Extract the User-Agent header field */ rc = ib_var_target_get_const( cfg->user_agent, &bs_list, tx->mp, tx->var_store ); if (rc == IB_ENOENT || ib_list_elements(bs_list) == 0) { ib_log_debug_tx(tx, "request_header_finished_event: No user agent"); return IB_OK; } if (rc != IB_OK) { ib_log_error_tx(tx, "Cannot retrieve request_headers:User-Agent: %d", rc); return rc; } if (IB_LIST_ELEMENTS(bs_list) == 0) { ib_log_debug_tx(tx, "handle_context_tx_event: No user agent"); return IB_OK; } req_agent = (ib_field_t *)IB_LIST_NODE_DATA(IB_LIST_LAST(bs_list)); /* Found it: copy the data into a newly allocated string buffer */ rc = ib_field_value_type(req_agent, ib_ftype_bytestr_out(&bs), IB_FTYPE_BYTESTR); if (rc != IB_OK) { ib_log_error_tx(tx, "Request user agent is not a BYTESTR: %s", ib_status_to_string(rc)); return rc; } /* Finally, split it up & store the components */ rc = modua_agent_fields(ib, tx, bs); return rc; }
/** * Parse the user agent header, splitting into component fields. * * Attempt to tokenize the user agent string passed in, storing the * result in the DPI associated with the transaction. * * @param[in] ib IronBee object * @param[in,out] tx Transaction object * @param[in] bs Byte string containing the agent string * * @returns Status code */ static ib_status_t modua_agent_fields(ib_engine_t *ib, ib_tx_t *tx, const ib_bytestr_t *bs) { const modua_match_rule_t *rule = NULL; ib_field_t *agent_list = NULL; char *product = NULL; char *platform = NULL; char *extra = NULL; char *agent; char *buf; size_t len; ib_status_t rc; ib_var_source_t *source; /* Get the length of the byte string */ len = ib_bytestr_length(bs); /* Allocate memory for a copy of the string to split up below. */ buf = (char *)ib_mpool_calloc(tx->mp, 1, len+1); if (buf == NULL) { ib_log_error_tx(tx, "Failed to allocate %zd bytes for agent string", len+1); return IB_EALLOC; } /* Copy the string out */ memcpy(buf, ib_bytestr_const_ptr(bs), len); buf[len] = '\0'; ib_log_debug_tx(tx, "Found user agent: '%s'", buf); /* Copy the agent string */ agent = (char *)ib_mpool_strdup(tx->mp, buf); if (agent == NULL) { ib_log_error_tx(tx, "Failed to allocate copy of agent string"); return IB_EALLOC; } /* Parse the user agent string */ rc = modua_parse_uastring(buf, &product, &platform, &extra); if (rc != IB_OK) { ib_log_debug_tx(tx, "Failed to parse User Agent string '%s'", agent); return IB_OK; } /* Categorize the parsed string */ rule = modua_match_cat_rules(product, platform, extra); if (rule == NULL) { ib_log_debug_tx(tx, "No rule matched" ); } else { ib_log_debug_tx(tx, "Matched to rule #%d / category '%s'", rule->rule_num, rule->category ); } /* Build a new list. */ rc = ib_var_source_acquire( &source, tx->mp, ib_engine_var_config_get(ib), IB_S2SL("UA") ); if (rc != IB_OK) { ib_log_alert_tx(tx, "Unable to acquire source for UserAgent list."); return rc; } rc = ib_var_source_initialize( source, &agent_list, tx->var_store, IB_FTYPE_LIST ); if (rc != IB_OK) { ib_log_alert_tx(tx, "Unable to add UserAgent list to DPI."); return rc; } /* Store Agent */ rc = modua_store_field(ib, tx->mp, agent_list, "agent", agent); if (rc != IB_OK) { return rc; } /* Store product */ rc = modua_store_field(ib, tx->mp, agent_list, "PRODUCT", product); if (rc != IB_OK) { return rc; } /* Store Platform */ rc = modua_store_field(ib, tx->mp, agent_list, "OS", platform); if (rc != IB_OK) { return rc; } /* Store Extra */ rc = modua_store_field(ib, tx->mp, agent_list, "extra", extra); if (rc != IB_OK) { return rc; } /* Store Extra */ if (rule != NULL) { rc = modua_store_field(ib, tx->mp, agent_list, "category", rule->category); } else { rc = modua_store_field(ib, tx->mp, agent_list, "category", NULL ); } if (rc != IB_OK) { return rc; } /* Done */ return IB_OK; }