Ctrl K
ComunidadGuías…

src/zms.cpp

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//
// ZoneMinder Streaming Server, $Date$, $Revision$
// Copyright (C) 2001-2008 Philip Coombes
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//

#include "zm.h"
#include "zm_crypt.h"
#include "zm_db.h"
#include "zm_user.h"
#include "zm_signal.h"
#include "zm_monitorstream.h"
#include "zm_eventstream.h"
#include "zm_fifo_stream.h"
#include <iomanip>
#include <sstream>
#include <string>
#include <unistd.h>

// Refuses the request with 403. As nph-zms under Apache (nph- handling) the
// raw status line reaches the client as written. Where the CGI headers are
// parsed, the Status header sets the status instead: plain zms under any
// server, and nph-zms behind nginx + fcgiwrap, where the raw line alone is not
// taken as the status and the refusal went out as an empty 200. In NPH mode
// both are written, so either server answers 403.
static void Forbidden(bool nph) {
  if (nph) fputs("HTTP/1.0 403 Forbidden\r\n", stdout);
  fputs("Status: 403 Forbidden\r\n\r\n", stdout);
}

bool ValidateAccess(User *user, int mon_id) {
  bool allowed = true;

  if (mon_id > 0) {
    if (user->getStream() < User::PERM_VIEW) {
      allowed = false;
      Warning("Insufficient privileges for request user %d %s for monitor %d, user does not have permission to stream live view.",
              user->Id(), user->getUsername(), mon_id);
    } else if (!user->canAccess(mon_id)) {
      allowed = false;
      Warning("Insufficient privileges for request user %d %s for monitor %d, user does not have permission view this monitor.",
              user->Id(), user->getUsername(), mon_id);
    }
  } else if (user->getEvents() < User::PERM_VIEW) {
    allowed = false;
    Warning("Insufficient privileges for request user %d %s, user does not have permission view events.",
            user->Id(), user->getUsername());
  }
  return allowed;
}

// Event access is decided by the monitor the event belongs to, never by a monitor id the
// client supplies alongside it: an allowed monitor paired with a denied monitor's event id
// must not stream that event. mon_id is used only when the stream is chosen by monitor and
// time, in which case it is the monitor whose events are streamed.
bool ValidateEventAccess(User *user, int mon_id, uint64_t event_id, bool by_monitor_and_time) {
  if (user->getEvents() < User::PERM_VIEW) {
    Warning("Insufficient privileges for request user %d %s, user does not have permission view events.",
            user->Id(), user->getUsername());
    return false;
  }
  if (!by_monitor_and_time) {
    zmDbRow row;
    if (!event_id || !row.fetch(stringtf("SELECT `MonitorId` FROM `Events` WHERE `Id` = %" PRIu64, event_id))) {
      Warning("Unknown event %" PRIu64 " requested by user %d %s", event_id, user->Id(), user->getUsername());
      return false;
    }
    mon_id = row[0] ? atoi(row[0]) : 0;
  }
  if (!user->canAccess(mon_id)) {
    Warning("Insufficient privileges for request user %d %s for event %" PRIu64 " of monitor %d, user does not have permission view this monitor.",
            user->Id(), user->getUsername(), event_id, mon_id);
    return false;
  }
  return true;
}

int main(int argc, const char *argv[], char **envp) {
  self = argv[0];

  srand(getpid() * time(nullptr));

  enum { ZMS_UNKNOWN, ZMS_MONITOR, ZMS_EVENT, ZMS_FIFO } source = ZMS_UNKNOWN;
  enum { ZMS_JPEG, ZMS_MPEG, ZMS_RAW, ZMS_ZIP, ZMS_SINGLE } mode = ZMS_JPEG;
  char format[32] = "";
  int monitor_id = 0;
  SystemTimePoint event_time = std::chrono::system_clock::time_point::min();
  uint64_t event_id = 0;
  unsigned int frame_id = 1;
  int frames_to_send = -1;
  unsigned int scale = 100;
  unsigned int rate = 100;
  double maxfps = 0.0;
  unsigned int bitrate = 100000;
  unsigned int ttl = 0;
  bool  analysis_frames = false;
  EventStream::StreamMode replay = EventStream::MODE_NONE;
  std::string username;
  std::string password;
  char auth[64] = "";
  std::string jwt_token_str = "";
  unsigned int connkey = 0;
  unsigned int playback_buffer = 0;

  bool nph = false;
  const char *basename = strrchr(argv[0], '/');
  if ( basename )  // if we found a / lets skip past it
    basename++;
  else  // argv[0] might not contain the full path, but just the script name
    basename = argv[0];
  const char *nph_prefix = "nph-";
  if ( basename && !strncmp(basename, nph_prefix, strlen(nph_prefix)) ) {
    nph = true;
  }

  char log_id_string[32] = "zms";
  logInit(log_id_string);
  zmLoadStaticConfig();
  zmDbConnect();
  zmLoadDBConfig();
  logInit(log_id_string);

  for (char **env = envp; *env != 0; env++) {
    char *thisEnv = *env;
    // The query string (user=/pass=, auth=, token=) also appears in
    // REQUEST_URI and can appear in HTTP_REFERER, and the cookie holds the
    // web session: log those without their credentials.
    const char *equals = strchr(thisEnv, '=');
    std::string name = equals ? std::string(thisEnv, equals - thisEnv) : thisEnv;
    if (equals && (name == "HTTP_COOKIE" || name.find("QUERY_STRING") != std::string::npos
                   || name.find("AUTHORIZATION") != std::string::npos)) {
      Debug(1, "env: %s=<not logged>", name.c_str());
    } else if (equals && strchr(equals, '?')) {
      Debug(1, "env: %s=%s", name.c_str(), url_for_log(equals + 1).c_str());
    } else {
      Debug(1, "env: %s", thisEnv);
    }
  }

  const char *query = getenv("QUERY_STRING");
  if ( query == nullptr ) {
    Fatal("No query string.");
    return exit_zm(0);
  }  // end if query

  char *q_ptr = (char *)query;
  char *parms[16];  // Shouldn't be more than this
  int parm_no = 0;
  while ( (parm_no < 16) && (parms[parm_no] = strtok(q_ptr, "&")) ) {
    parm_no++;
    q_ptr = nullptr;
  }

  for ( int p = 0; p < parm_no; p++ ) {
    char *name = strtok(parms[p], "=");
    char const *value = strtok(nullptr, "=");
    if ( !value )
      value = "";
    if ( !strcmp(name, "analysis") ) {
      if ( !strcmp(value, "true") ) {
        analysis_frames = true;
      } else {
        analysis_frames = (atoi(value) == 1);
      }
      Debug(1, "Viewing analysis frames");
    } else if ( !strcmp(name, "source") ) {
      if ( !strcmp(value, "event") ) {
        source = ZMS_EVENT;
      } else if ( !strcmp(value, "fifo") ) {
        source = ZMS_FIFO;
      } else {
        source = ZMS_MONITOR;
      }
    } else if ( !strcmp(name, "mode") ) {
      if (!strcmp(value, "jpeg")) {
        mode = ZMS_JPEG;
      } else if (!strcmp(value, "single")) {
        mode = ZMS_SINGLE;
      } else if (!strcmp(value, "raw")) {
        mode = ZMS_RAW;
      } else if (!strcmp(value, "zip")) {
        mode = ZMS_ZIP;
      } else if (!strcmp(value, "mpeg")) {
        mode = ZMS_MPEG;
      } else if (!strcmp(value, "paused")) {
        mode = ZMS_JPEG;
      } else {
        Error("Unsupported value for mode");
      }
    } else if ( !strcmp(name, "format") ) {
      strncpy(format, value, sizeof(format)-1);
    } else if ( !strcmp(name, "monitor") ) {
      monitor_id = atoi(value);
      if ( source == ZMS_UNKNOWN )
        source = ZMS_MONITOR;
    } else if ( !strcmp(name, "datetime") ) {
      std::tm tm = {};
      std::stringstream ss(value);
      ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
      event_time = std::chrono::system_clock::from_time_t(std::mktime(&tm));
      //Debug(1, "Setting event_time to %s", SystemTimePointToString(event_time).c_str());
    } else if ( !strcmp(name, "time") ) {
      double double_time = atof(value);
      std::chrono::duration<double> event_time_seconds(double_time);
      auto system_duration = std::chrono::duration_cast<std::chrono::system_clock::duration>(event_time_seconds);
	    event_time = SystemTimePoint{system_duration};
      //Debug(1, "Setting event_time to %s from %s %.2f %lf",
          //SystemTimePointToString(event_time).c_str(), value, double_time, event_time_seconds.count());
    } else if ( !strcmp(name, "event") ) {
      event_id = strtoull(value, nullptr, 10);
      source = ZMS_EVENT;
    } else if ( !strcmp(name, "frame") ) {
      frame_id = strtoull(value, nullptr, 10);
      source = ZMS_EVENT;
    } else if ( !strcmp(name, "frames") ) {
      frames_to_send = strtoll(value, nullptr, 10);
    } else if ( !strcmp(name, "scale") ) {
      scale = atoi(value);
      if (scale > 1600) {
        Warning("Limiting scale to 16x");
        scale = 1600;
      }
    } else if ( !strcmp(name, "rate") ) {
      rate = atoi(value);
    } else if ( !strcmp(name, "maxfps") ) {
      maxfps = atof(value);
    } else if ( !strcmp(name, "bitrate") ) {
      bitrate = atoi(value);
    } else if ( !strcmp(name, "ttl") ) {
      ttl = atoi(value);
    } else if ( !strcmp(name, "replay") ) {
      if ( !strcmp(value, "gapless") ) {
        replay = EventStream::MODE_ALL_GAPLESS;
      } else if ( !strcmp(value, "all") ) {
        replay = EventStream::MODE_ALL;
      } else if ( !strcmp(value, "none") ) {
        replay = EventStream::MODE_NONE;
      } else if ( !strcmp(value, "single") ) {
        replay = EventStream::MODE_SINGLE;
      } else {
        Error("Unsupported value %s for replay, defaulting to none", value);
      }
    } else if ( !strcmp(name, "connkey") ) {
      connkey = atoi(value);
    } else if ( !strcmp(name, "buffer") ) {
      playback_buffer = atoi(value);
    } else if ( !strcmp(name, "auth") ) {
      strncpy(auth, value, sizeof(auth)-1);
    } else if ( !strcmp(name, "token") ) {
      jwt_token_str = value;
      Debug(1, "ZMS: JWT token found: %s", zm::crypto::LogFingerprint(jwt_token_str).c_str());
    } else if ( !strcmp(name, "user") ) {
      username = UriDecode(value);
    } else if ( !strcmp(name, "username") ) {
      username = UriDecode(value);
    } else if ( !strcmp(name, "pass") ) {
      password = UriDecode(value);
      Debug(1, "Have a password");
    } else if ( !strcmp(name, "password") ) {
      password = UriDecode(value);
      Debug(1, "Have a password");
    } else {
      Debug(1, "Unknown parameter passed to zms %s=%s", name, value);
    }  // end if possible parameter names
  }  // end foreach parm

  if ( event_id ) {
    snprintf(log_id_string, sizeof(log_id_string), "zms_e%" PRIu64, event_id);
  } else if ( monitor_id ) {
    snprintf(log_id_string, sizeof(log_id_string), "zms_m%d", monitor_id);
  }
  logInit(log_id_string);

  // Summarise the decoded request so a runaway nph-zms can be tied to its
  // stream type/params. Debug level because this fires once per process and
  // there can be many (one per montage tile). refs #5006 (nph-zms
  // occasionally consuming excessive memory).
  {
    static const char *source_names[] = {"unknown", "monitor", "event", "fifo"};
    static const char *mode_names[] = {"jpeg", "mpeg", "raw", "zip", "single"};
    Debug(1, "zms request: source=%s mode=%s monitor=%d event=%" PRIu64
         " frame=%u frames=%d connkey=%u scale=%u rate=%u maxfps=%.2f"
         " buffer=%u ttl=%u rss=%zuKB",
         source_names[source], mode_names[mode], monitor_id, event_id,
         frame_id, frames_to_send, connkey, scale, rate, maxfps,
         playback_buffer, ttl, zm_get_rss_kb());
  }

  // The user zms serves; it names the command socket, so only that user's
  // ajax/stream.php requests reach this stream.
  int owner_id = 0;
  if ( config.opt_use_auth ) {
    User *user = nullptr;

    if ( jwt_token_str != "" ) {
      // user = zmLoadTokenUser(jwt_token_str, config.auth_hash_ips);
      user = zmLoadTokenUser(jwt_token_str, false);
    } else if ( strcmp(config.auth_relay, "none") == 0 ) {
      if ( checkUser(username.c_str()) ) {
        user = zmLoadUser(username.c_str());
      } else {
        Error("Bad username");
      }
    } else {
      if ( *auth ) {
        user = zmLoadAuthUser(auth, username, config.auth_hash_ips);
      } else if ( username.length() && password.length() ) {
        user = zmLoadUser(username.c_str(), password.c_str());
      }
    }
    if ( !user ) {
      Forbidden(nph);

      const char *referer = getenv("HTTP_REFERER");
      const char *request_uri = getenv("REQUEST_URI");
      const char *xff = getenv("HTTP_X_FORWARDED_FOR");
      const char *remote = getenv("REMOTE_ADDR");
      // Most failures here are stale auth hashes on long-lived <img src=nph-zms?...>
      // streams whose hash TTL expired; the browser keeps reconnecting with the
      // baked-in URL. Including user/auth-prefix/uri/xff makes the noise diagnosable
      // without flipping on Debug.
      // Log only the first 8 chars of the hash: enough to correlate, not enough to replay.
      // The uri and referer go without their query string, which holds the
      // credentials (pass=, the full auth=, token=); monitor and event say
      // which stream it was. This Warning reaches the Logs table by default.
      Warning("Unable to authenticate user (user='%s' auth='%.8s%s' monitor=%d event=%" PRIu64
              " uri='%s' referer='%s' xff='%s' remote='%s')",
              username.c_str(),
              auth,
              (*auth && strlen(auth) > 8) ? "..." : "",
              monitor_id, event_id,
              request_uri ? url_for_log(request_uri).c_str() : "",
              referer ? url_for_log(referer).c_str() : "",
              xff ? xff : "",
              remote ? remote : "");
      return exit_zm(0);
    }
    bool allowed;
    if ( source == ZMS_EVENT ) {
      // Mirrors the setStreamStart() choice below.
      bool by_monitor_and_time = monitor_id && (event_time != std::chrono::system_clock::time_point::min());
      allowed = ValidateEventAccess(user, monitor_id, event_id, by_monitor_and_time);
    } else {
      allowed = ValidateAccess(user, monitor_id);
    }
    if ( !allowed ) {
      delete user;
      user = nullptr;
      Forbidden(nph);
      return exit_zm(0);
    }
    owner_id = user->Id();
    delete user;
    user = nullptr;
  }  // end if config.opt_use_auth

  HwCapsDetect();
  Image::Initialise();
  zmSetDefaultTermHandler();
  zmSetDefaultDieHandler();
  zmSetDefaultPipeHandler();

  setbuf(stdout, nullptr);
  if ( nph ) {
    fputs("HTTP/1.0 200 OK\r\n", stdout);
  }
  fprintf(stdout, "Server: ZoneMinder Video Server/%s\r\n", ZM_VERSION);

  time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
  char date_string[64];
  tm now_tm = {};
  strftime(date_string, sizeof(date_string)-1,
           "%a, %d %b %Y %H:%M:%S GMT", gmtime_r(&now, &now_tm));

  fputs("Last-Modified: ", stdout);
  fputs(date_string, stdout);
  fputs(
    "\r\nExpires: Mon, 26 Jul 1997 05:00:00 GMT\r\n"
    "Cache-Control: no-store, no-cache, must-revalidate\r\n"
    "Cache-Control: post-check=0, pre-check=0\r\n"
    "Pragma: no-cache\r\n",
    stdout);

  if ( source == ZMS_MONITOR ) {
    MonitorStream stream;
    stream.setStreamScale(scale);
    stream.setStreamReplayRate(rate);
    stream.setStreamMaxFPS(maxfps);
    stream.setStreamTTL(ttl);
    stream.setStreamQueue(connkey);
    stream.setStreamOwner(owner_id);
    stream.setStreamBuffer(playback_buffer);
    stream.setFramesToSend(frames_to_send);
    stream.setStreamStart(monitor_id);
    stream.setStreamFrameType(analysis_frames ? StreamBase::FRAME_ANALYSIS: StreamBase::FRAME_NORMAL);

    if ( mode == ZMS_JPEG ) {
      stream.setStreamType(MonitorStream::STREAM_JPEG);
    } else if ( mode == ZMS_RAW ) {
      stream.setStreamType(MonitorStream::STREAM_RAW);
    } else if ( mode == ZMS_ZIP ) {
      stream.setStreamType(MonitorStream::STREAM_ZIP);
    } else if ( mode == ZMS_SINGLE ) {
      stream.setStreamType(MonitorStream::STREAM_SINGLE);
    } else {
      stream.setStreamFormat(format);
      stream.setStreamBitrate(bitrate);
      stream.setStreamType(MonitorStream::STREAM_MPEG);
    }
    stream.runStream();
  } else if ( source == ZMS_FIFO ) {
    FifoStream stream;
    stream.setStreamMaxFPS(maxfps);
    stream.setStreamStart(monitor_id, format);
    stream.runStream();
  } else if ( source == ZMS_EVENT ) {
    EventStream stream;
    stream.setStreamScale(scale);
    stream.setStreamReplayRate(rate);
    stream.setStreamMaxFPS(maxfps);
    stream.setStreamMode(replay);
    stream.setStreamQueue(connkey);
    stream.setStreamOwner(owner_id);
    stream.setFramesToSend(frames_to_send);
    if ( monitor_id && (event_time != std::chrono::system_clock::time_point::min())) {
      stream.setStreamStart(monitor_id, event_time);
    } else if ( event_id && (event_time != std::chrono::system_clock::time_point::min())) {
      stream.setStreamStart(event_id, event_time);
    } else {
      Debug(3, "Setting stream start to frame (%d)", frame_id);
      stream.setStreamStart(event_id, frame_id);
    }
    stream.setStreamFrameType(analysis_frames ? StreamBase::FRAME_ANALYSIS: StreamBase::FRAME_NORMAL);
    if ( mode == ZMS_JPEG ) {
      stream.setStreamType(EventStream::STREAM_JPEG);
    } else if ( mode == ZMS_SINGLE ) {
      stream.setStreamType(MonitorStream::STREAM_SINGLE);
    } else {
      stream.setStreamFormat(format);
      stream.setStreamBitrate(bitrate);
      stream.setStreamType(EventStream::STREAM_MPEG);
    }  // end if jpeg or mpeg
    stream.runStream();
  } else {
    Error("Neither a monitor or event was specified.");
  }  // end if monitor or event

  return exit_zm(0);
}