// See the file "COPYING" in the main distribution directory for copyright. #include "zeek-config.h" #include #ifdef TIME_WITH_SYS_TIME # include # include #else # ifdef HAVE_SYS_TIME_H # include # else # include # endif #endif #include #include #include #include #include #include #include #include #include "File.h" #include "Type.h" #include "Timer.h" #include "Expr.h" #include "NetVar.h" #include "Net.h" #include "Serializer.h" #include "Event.h" #include "Reporter.h" // Timer which on dispatching rotates the file. class RotateTimer : public Timer { public: RotateTimer(double t, BroFile* f, bool arg_raise) : Timer(t, TIMER_ROTATE) { file = f; raise = arg_raise; name = copy_string(f->Name()); } ~RotateTimer(); void Dispatch(double t, int is_expire); protected: BroFile* file; bool raise; const char* name; }; RotateTimer::~RotateTimer() { if ( file->rotate_timer == this ) file->rotate_timer = 0; delete [] name; } void RotateTimer::Dispatch(double t, int is_expire) { file->rotate_timer = 0; if ( ! is_expire ) { if ( raise ) { Ref(file); mgr.QueueEvent(rotate_interval, {new Val(file)}); } file->InstallRotateTimer(); } } // The following could in principle be part of a "file manager" object. #define MAX_FILE_CACHE_SIZE 512 static int num_files_in_cache = 0; static BroFile* head = 0; static BroFile* tail = 0; double BroFile::default_rotation_interval = 0; double BroFile::default_rotation_size = 0; // Maximizes the number of open file descriptors and returns the number // that we should use for the cache. static int maximize_num_fds() { struct rlimit rl; if ( getrlimit(RLIMIT_NOFILE, &rl) < 0 ) reporter->FatalError("maximize_num_fds(): getrlimit failed"); if ( rl.rlim_max == RLIM_INFINITY ) { // Don't try raising the current limit. if ( rl.rlim_cur == RLIM_INFINITY ) // Let's not be too ambitious. return MAX_FILE_CACHE_SIZE; else return rl.rlim_cur / 2; } // See if we can raise the current to the maximum. rl.rlim_cur = rl.rlim_max; if ( setrlimit(RLIMIT_NOFILE, &rl) < 0 ) reporter->FatalError("maximize_num_fds(): setrlimit failed"); return rl.rlim_cur / 2; } BroFile::BroFile(FILE* arg_f) { Init(); f = arg_f; name = access = 0; t = base_type(TYPE_STRING); is_open = (f != 0); if ( f ) UpdateFileSize(); } BroFile::BroFile(FILE* arg_f, const char* arg_name, const char* arg_access) { Init(); f = arg_f; name = copy_string(arg_name); access = copy_string(arg_access); t = base_type(TYPE_STRING); is_open = (f != 0); if ( f ) UpdateFileSize(); } BroFile::BroFile(const char* arg_name, const char* arg_access, BroType* arg_t) { Init(); f = 0; name = copy_string(arg_name); access = copy_string(arg_access); t = arg_t ? arg_t : base_type(TYPE_STRING); if ( streq(name, "/dev/stdin") ) f = stdin; else if ( streq(name, "/dev/stdout") ) f = stdout; else if ( streq(name, "/dev/stderr") ) f = stderr; if ( f ) is_open = 1; else if ( ! Open() ) { reporter->Error("cannot open %s: %s", name, strerror(errno)); is_open = 0; okay_to_manage = 0; } } const char* BroFile::Name() const { if ( name ) return name; if ( f == stdin ) return"/dev/stdin"; if ( f == stdout ) return "/dev/stdout"; if ( f == stderr ) return "/dev/stderr"; return 0; } bool BroFile::Open(FILE* file, const char* mode) { open_time = network_time ? network_time : current_time(); if ( ! max_files_in_cache ) // Haven't initialized yet. max_files_in_cache = maximize_num_fds(); if ( num_files_in_cache >= max_files_in_cache ) PurgeCache(); f = file; if ( default_rotation_interval && (! attrs || ! attrs->FindAttr(ATTR_ROTATE_INTERVAL)) ) rotate_interval = default_rotation_interval; if ( default_rotation_size && (! attrs || ! attrs->FindAttr(ATTR_ROTATE_SIZE)) ) rotate_size = default_rotation_size; InstallRotateTimer(); if ( ! f ) { if ( ! mode ) f = fopen(name, access); else f = fopen(name, mode); } SetBuf(buffered); if ( f ) { // These are the only files we manage, because we open them // ourselves and hence don't have any surprises regarding // whether we're allowed to close them. is_open = okay_to_manage = 1; InsertAtBeginning(); UpdateFileSize(); } else { // No point managing it. is_open = okay_to_manage = 0; return false; } RaiseOpenEvent(); return true; } BroFile::~BroFile() { Close(); Unref(t); Unref(attrs); delete [] name; delete [] access; delete [] cipher_buffer; #ifdef USE_PERFTOOLS_DEBUG heap_checker->UnIgnoreObject(this); #endif } void BroFile::Init() { is_open = okay_to_manage = is_in_cache = 0; position = 0; next = prev = 0; rotate_timer = 0; rotate_interval = 0.0; rotate_size = current_size = 0.0; open_time = 0; attrs = 0; buffered = true; print_hook = true; raw_output = false; t = 0; pub_key = 0; cipher_ctx = 0; cipher_buffer = 0; #ifdef USE_PERFTOOLS_DEBUG heap_checker->IgnoreObject(this); #endif } FILE* BroFile::File() { if ( okay_to_manage && ! is_in_cache ) f = BringIntoCache(); return f; } FILE* BroFile::BringIntoCache() { char buf[256]; if ( f ) reporter->InternalError("BroFile non-nil non-open file"); if ( num_files_in_cache >= max_files_in_cache ) PurgeCache(); if ( position == 0 ) // Need to truncate it. f = fopen(name, access); else // Don't clobber it. f = fopen(name, "a"); if ( ! f ) { bro_strerror_r(errno, buf, sizeof(buf)); reporter->Error("can't open %s: %s", name, buf); f = fopen("/dev/null", "w"); if ( f ) { okay_to_manage = 0; return f; } bro_strerror_r(errno, buf, sizeof(buf)); reporter->Error("can't open /dev/null: %s", buf); return 0; } RaiseOpenEvent(); UpdateFileSize(); if ( fseek(f, position, SEEK_SET) < 0 ) { bro_strerror_r(errno, buf, sizeof(buf)); reporter->Error("reopen seek failed: %s", buf); } InsertAtBeginning(); return f; } FILE* BroFile::Seek(long new_position) { if ( ! File() ) return 0; if ( fseek(f, new_position, SEEK_SET) < 0 ) reporter->Error("seek failed"); return f; } void BroFile::SetBuf(bool arg_buffered) { if ( ! f ) return; if ( setvbuf(f, NULL, arg_buffered ? _IOFBF : _IOLBF, 0) != 0 ) reporter->Error("setvbuf failed"); buffered = arg_buffered; } int BroFile::Close() { if ( rotate_timer ) { timer_mgr->Cancel(rotate_timer); rotate_timer = 0; } if ( ! is_open ) return 1; FinishEncrypt(); // Do not close stdin/stdout/stderr. if ( f == stdin || f == stdout || f == stderr ) return 0; if ( is_in_cache ) { Unlink(); if ( f ) { fclose(f); f = 0; open_time = 0; } is_open = 0; okay_to_manage = 0; // no longer managed since will never reopen return 1; } // Not managed. if ( ! f ) return 0; fclose(f); f = 0; return 1; } void BroFile::Suspend() { if ( ! is_in_cache ) reporter->InternalError("BroFile::Suspend() called for non-cached file"); if ( ! is_open ) reporter->InternalError("BroFile::Suspend() called for non-open file"); Unlink(); if ( ! f ) reporter->InternalError("BroFile::Suspend() called for nil file"); if ( (position = ftell(f)) < 0 ) { char buf[256]; bro_strerror_r(errno, buf, sizeof(buf)); reporter->Error("ftell failed: %s", buf); position = 0; } fclose(f); f = 0; } void BroFile::PurgeCache() { if ( tail ) { tail->Suspend(); return; } reporter->InternalWarning("BroFile purge of empty cache"); } void BroFile::Unlink() { if ( is_in_cache ) { if ( head == this ) head = Next(); else Prev()->SetNext(next); if ( tail == this ) tail = Prev(); else Next()->SetPrev(prev); if ( (head || tail) && ! (head && tail) ) reporter->InternalError("BroFile link list botch"); is_in_cache = 0; prev = next = 0; if ( --num_files_in_cache < 0 ) reporter->InternalError("BroFile underflow of file cache"); } } void BroFile::InsertAtBeginning() { if ( ! head ) { head = tail = this; next = prev = 0; } else { SetNext(head); SetPrev(0); head->SetPrev(this); head = this; } if ( ++num_files_in_cache > max_files_in_cache ) reporter->InternalError("BroFile overflow of file cache"); is_in_cache = 1; } void BroFile::MoveToBeginning() { if ( head == this ) return; // already at the beginning if ( ! is_in_cache || ! prev ) reporter->InternalError("BroFile inconsistency in MoveToBeginning()"); Unlink(); InsertAtBeginning(); } void BroFile::Describe(ODesc* d) const { d->AddSP("file"); if ( name ) { d->Add("\""); d->Add(name); d->AddSP("\""); } d->AddSP("of"); if ( t ) t->Describe(d); else d->Add("(no type)"); } void BroFile::SetAttrs(Attributes* arg_attrs) { if ( ! arg_attrs ) return; attrs = arg_attrs; Ref(attrs); Attr* ef = attrs->FindAttr(ATTR_ROTATE_INTERVAL); if ( ef ) rotate_interval = ef->AttrExpr()->ExprVal()->AsInterval(); ef = attrs->FindAttr(ATTR_ROTATE_SIZE); if ( ef ) rotate_size = ef->AttrExpr()->ExprVal()->AsDouble(); ef = attrs->FindAttr(ATTR_ENCRYPT); if ( ef ) { if ( ef->AttrExpr() ) InitEncrypt(ef->AttrExpr()->ExprVal()->AsString()->CheckString()); else InitEncrypt(opt_internal_string("log_encryption_key")->CheckString()); } if ( attrs->FindAttr(ATTR_RAW_OUTPUT) ) EnableRawOutput(); InstallRotateTimer(); } void BroFile::SetRotateInterval(double secs) { rotate_interval = secs; InstallRotateTimer(); } RecordVal* BroFile::Rotate() { if ( ! is_open ) return 0; // Do not rotate stdin/stdout/stderr. if ( f == stdin || f == stdout || f == stderr ) return 0; if ( okay_to_manage && ! is_in_cache ) BringIntoCache(); RecordVal* info = new RecordVal(rotate_info); FILE* newf = rotate_file(name, info); if ( ! newf ) { Unref(info); return 0; } info->Assign(2, new Val(open_time, TYPE_TIME)); Unlink(); fclose(f); f = 0; Open(newf); return info; } void BroFile::InstallRotateTimer() { if ( terminating ) return; if ( rotate_timer ) { timer_mgr->Cancel(rotate_timer); rotate_timer = 0; } if ( rotate_interval ) { // When this is called for the first time, network_time can // still be zero. If so, we set a timer which fires // immediately but doesn't rotate when it expires. if ( ! network_time ) rotate_timer = new RotateTimer(1, this, false); else { if ( ! open_time ) open_time = network_time; const char* base_time = log_rotate_base_time ? log_rotate_base_time->AsString()->CheckString() : 0; double base = parse_rotate_base_time(base_time); double delta_t = calc_next_rotate(network_time, rotate_interval, base); rotate_timer = new RotateTimer(network_time + delta_t, this, true); } timer_mgr->Add(rotate_timer); } } void BroFile::SetDefaultRotation(double interval, double max_size) { for ( BroFile* f = head; f; f = f->next ) { if ( ! (f->attrs && f->attrs->FindAttr(ATTR_ROTATE_INTERVAL)) ) { f->rotate_interval = interval; f->InstallRotateTimer(); } if ( ! (f->attrs && f->attrs->FindAttr(ATTR_ROTATE_SIZE)) ) f->rotate_size = max_size; } default_rotation_interval = interval; default_rotation_size = max_size; } void BroFile::CloseCachedFiles() { BroFile* next; for ( BroFile* f = head; f; f = next ) { // Send final rotate events (immediately). if ( f->rotate_interval ) { Ref(f); Event* event = new Event(::rotate_interval, {new Val(f)}); mgr.Dispatch(event, true); } if ( f->rotate_size ) { Ref(f); Event* event = new ::Event(::rotate_size, {new Val(f)}); mgr.Dispatch(event, true); } next = f->next; if ( f->is_in_cache ) f->Close(); } } void BroFile::InitEncrypt(const char* keyfile) { if ( ! (pub_key || keyfile) ) return; if ( ! pub_key ) { FILE* key = fopen(keyfile, "r"); if ( ! key ) { reporter->Error("can't open key file %s: %s", keyfile, strerror(errno)); Close(); return; } pub_key = PEM_read_PUBKEY(key, 0, 0, 0); if ( ! pub_key ) { reporter->Error("can't read key from %s: %s", keyfile, ERR_error_string(ERR_get_error(), 0)); Close(); return; } } // Depending on the OpenSSL version, EVP_*_cbc() // returns a const or a non-const. EVP_CIPHER* cipher_type = (EVP_CIPHER*) EVP_bf_cbc(); cipher_ctx = EVP_CIPHER_CTX_new(); unsigned char secret[EVP_PKEY_size(pub_key)]; unsigned char* psecret = secret; unsigned int secret_len; int iv_len = EVP_CIPHER_iv_length(cipher_type); unsigned char iv[iv_len]; if ( ! EVP_SealInit(cipher_ctx, cipher_type, &psecret, (int*) &secret_len, iv, &pub_key, 1) ) { reporter->Error("can't init cipher context for %s: %s", keyfile, ERR_error_string(ERR_get_error(), 0)); Close(); return; } secret_len = htonl(secret_len); if ( fwrite("BROENC1", 7, 1, f) < 1 || fwrite(&secret_len, sizeof(secret_len), 1, f) < 1 || fwrite(secret, ntohl(secret_len), 1, f) < 1 || fwrite(iv, iv_len, 1, f) < 1 ) { reporter->Error("can't write header to log file %s: %s", name, strerror(errno)); Close(); return; } int buf_size = MIN_BUFFER_SIZE + EVP_CIPHER_block_size(cipher_type); cipher_buffer = new unsigned char[buf_size]; } void BroFile::FinishEncrypt() { if ( ! is_open ) return; if ( ! pub_key ) return; if ( cipher_ctx ) { int outl; EVP_SealFinal(cipher_ctx, cipher_buffer, &outl); if ( outl && fwrite(cipher_buffer, outl, 1, f) < 1 ) { reporter->Error("write error for %s: %s", name, strerror(errno)); return; } EVP_CIPHER_CTX_free(cipher_ctx); cipher_ctx = 0; } } int BroFile::Write(const char* data, int len) { if ( ! is_open ) return 0; if ( ! is_in_cache && okay_to_manage ) BringIntoCache(); if ( ! len ) len = strlen(data); if ( cipher_ctx ) { while ( len ) { int outl; int inl = min(+MIN_BUFFER_SIZE, len); if ( ! EVP_SealUpdate(cipher_ctx, cipher_buffer, &outl, (unsigned char*)data, inl) ) { reporter->Error("encryption error for %s: %s", name, ERR_error_string(ERR_get_error(), 0)); Close(); return 0; } if ( outl && fwrite(cipher_buffer, outl, 1, f) < 1 ) { reporter->Error("write error for %s: %s", name, strerror(errno)); Close(); return 0; } data += inl; len -= inl; } return 1; } if ( fwrite(data, len, 1, f) < 1 ) return false; if ( rotate_size && current_size < rotate_size && current_size + len >= rotate_size ) { mgr.QueueEvent(::rotate_size, {new Val(this)}); } // This does not work if we seek around. But none of the logs does that // and we avoid stat()'ing the file all the time. current_size += len; return true; } void BroFile::RaiseOpenEvent() { if ( ! ::file_opened ) return; Ref(this); Event* event = new ::Event(::file_opened, {new Val(this)}); mgr.Dispatch(event, true); } void BroFile::UpdateFileSize() { struct stat s; if ( fstat(fileno(f), &s) < 0 ) { reporter->Error("can't stat fd for %s: %s", name, strerror(errno)); current_size = 0; return; } current_size = double(s.st_size); } bool BroFile::Serialize(SerialInfo* info) const { return SerialObj::Serialize(info); } BroFile* BroFile::GetFile(const char* name) { for ( BroFile* f = head; f; f = f->next ) { if ( f->name && streq(name, f->name) ) return f; } return new BroFile(name, "w", 0); } BroFile* BroFile::Unserialize(UnserialInfo* info) { BroFile* file = (BroFile*) SerialObj::Unserialize(info, SER_BRO_FILE); if ( ! file ) return 0; if ( file->is_open ) return file; // If there is already an object for this file, return it. if ( file->name ) { for ( BroFile* f = head; f; f = f->next ) { if ( f->name && streq(file->name, f->name) ) { Unref(file); Ref(f); return f; } } } // Otherwise, open, but don't clobber. if ( ! file->Open(0, "a") ) { info->s->Error(fmt("cannot open %s: %s", file->name, strerror(errno))); return 0; } // Here comes a hack. This method will return a pointer to a newly // instantiated file object. As soon as this pointer is Unref'ed, the // file will be closed. That means that when we unserialize the same // file next time, we will re-open it and thereby delete the first one, // i.e., we will be keeping to delete what we've written just before. // // To avoid this loop, we do an extra Ref here, i.e., this file will // *never* be closed anymore (as long the file cache does not overflow). Ref(file); // We deliberately override log rotation attributes with our defaults. file->rotate_interval = log_rotate_interval; file->rotate_size = log_max_size; file->InstallRotateTimer(); file->SetBuf(file->buffered); return file; } IMPLEMENT_SERIAL(BroFile, SER_BRO_FILE); bool BroFile::DoSerialize(SerialInfo* info) const { DO_SERIALIZE(SER_BRO_FILE, BroObj); const char* s = name; if ( ! okay_to_manage ) { // We can handle stdin/stdout/stderr but no others. if ( f == stdin ) s = "/dev/stdin"; else if ( f == stdout ) s = "/dev/stdout"; else if ( f == stderr ) s = "/dev/stderr"; else { // We don't manage the file, and therefore don't // really know how to pass it on to the other side. // However, in order to not abort communication // when this happens, we still send the name if we // have one; or if we don't, we create a special // "dont-have-a-file" file to be created on the // receiver side. if ( ! s ) s = "unmanaged-bro-output-file.log"; } } if ( ! (SERIALIZE(s) && SERIALIZE(buffered)) ) return false; SERIALIZE_OPTIONAL_STR(access); if ( ! t->Serialize(info) ) return false; SERIALIZE_OPTIONAL(attrs); return true; } bool BroFile::DoUnserialize(UnserialInfo* info) { DO_UNSERIALIZE(BroObj); if ( ! (UNSERIALIZE_STR(&name, 0) && UNSERIALIZE(&buffered)) ) return false; UNSERIALIZE_OPTIONAL_STR(access); t = BroType::Unserialize(info); if ( ! t ) return false; UNSERIALIZE_OPTIONAL(attrs, Attributes::Unserialize(info)); // Parse attributes. SetAttrs(attrs); // SetAttrs() has ref'ed attrs again. Unref(attrs); // Bind stdin/stdout/stderr. FILE* file = 0; is_open = false; f = 0; if ( streq(name, "/dev/stdin") ) file = stdin; else if ( streq(name, "/dev/stdout") ) file = stdout; else if ( streq(name, "/dev/stderr") ) file = stderr; if ( file ) { delete [] name; name = 0; f = file; is_open = true; } return true; }