/* * Copyright (c) 2026 Oninit LLC. All rights reserved. * * This source code is PROPRIETARY to Oninit LLC. * * NOT FOR DISTRIBUTION. * NOT FOR REUSE. * NOT FOR RESALE. * * No part of this source file may be copied, modified, distributed, * reused, resold, sublicensed, or made available to any third party * in any form, in whole or in part, without the express prior * written consent of Oninit LLC. */ /* dblogin: test an Informix login; print one JSON object or raw line. * Build: esql -o dblogin dblogin.ec * Add -DDBLOGIN_PROMPT_MAX=N to override the platform's password * prompt limit (0 = no limit). * Usage: dblogin ALIAS (username and password as two lines on stdin; * the last line may omit its newline; CRLF ok) * Or: dblogin --server ALIAS --username USER [--password PASS] * [--sqlhosts PATH] [--database NAME] [--output json|raw] * Omitting --password prompts on the terminal without echo; a prompted * password that reaches the platform limit is refused as possibly truncated. * Output: json {"success":true,"sqlcode":0,"current":...,"host":...,"version":...} * raw CURRENT HOST VERSION (fields separated by two spaces) * Exit: 0 success, 1 SQL failure, 2 input/usage failure. * VERSION 1.1 */ /* Feature-test macros so strict builds (-std=c99 etc.) still declare * setenv, getpass/getpassphrase and uname. The platform macros undo the * narrowing that _XOPEN_SOURCE causes on each system. esql may emit its * own #includes ahead of these; if a strict build still complains, pass * the same -D flags on the esql command line instead. */ #if !defined(_XOPEN_SOURCE) # define _XOPEN_SOURCE 600 #endif #if defined(__linux__) && !defined(_DEFAULT_SOURCE) # define _DEFAULT_SOURCE 1 /* glibc hides getpass() under XPG6 */ #endif #if defined(__sun) && !defined(__EXTENSIONS__) # define __EXTENSIONS__ 1 /* keeps getpassphrase() visible */ #endif #if defined(_AIX) && !defined(_ALL_SOURCE) # define _ALL_SOURCE 1 #endif #if defined(__hpux) && !defined(_HPUX_SOURCE) # define _HPUX_SOURCE 1 #endif #if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE) # define _DARWIN_C_SOURCE 1 #endif #include #include #include #include #include #include #include /* Longest password the platform's no-echo prompt returns without * truncating; 0 means no limit. Override with -DDBLOGIN_PROMPT_MAX=N. */ #if defined(DBLOGIN_PROMPT_MAX) # define PROMPT_MAX DBLOGIN_PROMPT_MAX #elif defined(__sun) /* Solaris getpass() stops at 8; getpassphrase() allows 256. */ # define PROMPT_MAX 256 #elif defined(__linux__) # define PROMPT_MAX 0 /* glibc getpass() has no fixed limit */ #elif defined(PASS_MAX) # define PROMPT_MAX PASS_MAX /* AIX, HP-UX and others publish this */ #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) # define PROMPT_MAX 128 /* BSD _PASSWORD_LEN */ #else # define PROMPT_MAX 8 /* unknown: assume the historic limit */ #endif #if defined(__sun) # define PROMPT_PASSWORD(p) getpassphrase(p) #else # define PROMPT_PASSWORD(p) getpass(p) #endif EXEC SQL include sqlca; EXEC SQL BEGIN DECLARE SECTION; static char database_name[512]; static char login_user[1024]; static char login_password[4096]; static char current_value[64]; static char connected_host[512]; static char server_version[512]; EXEC SQL END DECLARE SECTION; static int output_raw = 0; static void trim_right(char *value) { size_t length = strlen(value); while (length && (value[length - 1] == ' ' || value[length - 1] == '\t' || value[length - 1] == '\r' || value[length - 1] == '\n')) value[--length] = '\0'; } static void raw_string(const char *value) { const unsigned char *p = (const unsigned char *)value; for (; *p; ++p) putchar(*p < 32 || *p == 127 ? ' ' : *p); } static void json_string(const char *value) { const unsigned char *p = (const unsigned char *)value; putchar('"'); for (; *p; ++p) { if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); } else if (*p < 32) printf("\\u%04x", (unsigned int)*p); else putchar(*p); } putchar('"'); } /* Read one line. The last line may end at EOF without a newline; a line * longer than the buffer is rejected. A CR before the newline is dropped * so CRLF input works. */ static int read_value(char *buffer, size_t capacity) { size_t length; if (!fgets(buffer, (int)capacity, stdin)) return 0; length = strlen(buffer); if (length && buffer[length - 1] == '\n') buffer[--length] = '\0'; else if (length + 1 >= capacity) { /* Buffer full: accept only if the line ends right here. */ int next = getc(stdin); if (next != EOF && next != '\n') return 0; } if (length && buffer[length - 1] == '\r') buffer[--length] = '\0'; return length > 0; } /* Copy an Informix message template, replacing each %s with the error * parameter. Other % sequences are copied literally, so the template is * never used as a printf format. */ static void fill_message(char *out, size_t capacity, const char *tmpl, const char *param) { size_t used = 0; if (!capacity) return; while (*tmpl && used + 1 < capacity) { if (tmpl[0] == '%' && tmpl[1] == 's') { const char *q = param; while (*q && used + 1 < capacity) out[used++] = *q++; tmpl += 2; } else { out[used++] = *tmpl++; } } out[used] = '\0'; } static int sql_failure(const char *stage) { long code = sqlca.sqlcode; long isam = sqlca.sqlerrd[1]; char tmpl[2048]; char param[sizeof(sqlca.sqlerrm) + 1]; char message[2048]; mint length = 0; /* sqlerrm is a fixed array; copy it bounded and terminated. */ memcpy(param, sqlca.sqlerrm, sizeof(sqlca.sqlerrm)); param[sizeof(sqlca.sqlerrm)] = '\0'; trim_right(param); memset(tmpl, 0, sizeof(tmpl)); if (rgetlmsg((int4)code, tmpl, sizeof(tmpl) - 1, &length) != 0) snprintf(message, sizeof(message), "Informix SQL error %ld", code); else { trim_right(tmpl); fill_message(message, sizeof(message), tmpl, param); } if (output_raw) { printf("FAIL sqlcode=%ld isam=%ld stage=%s error=", code, isam, stage); raw_string(message); putchar('\n'); } else { printf("{\"success\":false,\"sqlcode\":%ld,\"isam\":%ld,\"stage\":", code, isam); json_string(stage); printf(",\"error\":"); json_string(message); puts("}"); } /* Error details are already captured, so DISCONNECT may reuse sqlca. */ if (strcmp(stage, "connect") != 0) { EXEC SQL DISCONNECT ALL; } return 1; } static int input_failure(const char *message) { if (output_raw) { printf("FAIL sqlcode=0 error="); raw_string(message); putchar('\n'); return 2; } printf("{\"success\":false,\"sqlcode\":0,\"error\":"); json_string(message); puts("}"); return 2; } /* A prompted password that fills the platform limit may have been cut * short without warning; refuse it rather than fail with a bad login. */ static int prompt_limit_failure(const char *password) { #if PROMPT_MAX > 0 static char message[512]; struct utsname host; if (strlen(password) < (size_t)PROMPT_MAX) return 0; if (uname(&host) != 0) strcpy(host.sysname, "this OS"); snprintf(message, sizeof(message), "Password reached the %s prompt limit of %d characters and may have " "been truncated; use stdin mode or --password", host.sysname, PROMPT_MAX); return input_failure(message); #else (void)password; return 0; #endif } /* Options that consume the following argument. */ static int takes_value(const char *option) { static const char *const names[] = { "--server", "--informixserver", "--username", "--user", "--password", "--sqlhosts", "--database", "--output", NULL }; int n; for (n = 0; names[n]; ++n) if (!strcmp(option, names[n])) return 1; return 0; } static void usage(void) { puts("dblogin VERSION 1.1\n" "Usage: dblogin ALIAS (username and password as two lines on stdin)\n" " or: dblogin --server ALIAS --username USER\n" " [--password PASS] [--sqlhosts PATH] [--database NAME] [--output json|raw]\n" "Without --password, prompt on the terminal without echo.\n" "Database defaults to sysmaster.\n" "Raw output: CURRENT HOST VERSION (separated by two spaces).\n" "Exit status: 0 success, 1 SQL failure, 2 input or usage failure."); } int main(int argc, char **argv) { const char *server = NULL; const char *username = NULL; const char *password = NULL; const char *sqlhosts = NULL; const char *database = "sysmaster"; int i; /* Choose formatting before validation so command errors honor raw mode. */ /* Walk option/value pairs so a value such as "--output" is never * mistaken for an option. */ for (i = 1; i + 1 < argc; ++i) if (takes_value(argv[i])) { if (!strcmp(argv[i], "--output")) output_raw = !strcmp(argv[i + 1], "raw"); ++i; } if (argc == 2 && argv[1][0] != '-') { server = argv[1]; if (!read_value(login_user, sizeof(login_user)) || !read_value(login_password, sizeof(login_password))) return input_failure("Invalid credentials input"); } else { for (i = 1; i < argc; ++i) { if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { usage(); return 0; } if (!takes_value(argv[i])) return input_failure("Unknown option; use --help"); if (i + 1 >= argc) return input_failure("Missing option value"); if (!strcmp(argv[i], "--server") || !strcmp(argv[i], "--informixserver")) server = argv[++i]; else if (!strcmp(argv[i], "--username") || !strcmp(argv[i], "--user")) username = argv[++i]; else if (!strcmp(argv[i], "--password")) password = argv[++i]; else if (!strcmp(argv[i], "--sqlhosts")) sqlhosts = argv[++i]; else if (!strcmp(argv[i], "--database")) database = argv[++i]; else if (!strcmp(argv[i], "--output")) { const char *format = argv[++i]; if (strcmp(format, "json") && strcmp(format, "raw")) return input_failure("Output must be json or raw"); output_raw = !strcmp(format, "raw"); } else return input_failure("Unknown option; use --help"); } if (!server || !username || !*username || strlen(username) >= sizeof(login_user)) return input_failure("Provide --server and --username"); if (sqlhosts && (!*sqlhosts || access(sqlhosts, R_OK) != 0)) return input_failure("SQLHOSTS file is not readable"); /* The CSDK can cache its environment before main(). Establish it * and exec again before prompting or attempting any connection. */ { const char *current_sqlhosts = getenv("INFORMIXSQLHOSTS"); const char *current_server = getenv("INFORMIXSERVER"); int restart = (sqlhosts && (!current_sqlhosts || strcmp(current_sqlhosts, sqlhosts))) || !current_server || strcmp(current_server, server); if (restart) { if ((sqlhosts && setenv("INFORMIXSQLHOSTS", sqlhosts, 1) != 0) || setenv("INFORMIXSERVER", server, 1) != 0) return input_failure("Cannot set Informix connection environment"); execvp(argv[0], argv); return input_failure("Cannot restart login helper with connection environment"); } } if (!password) { /* The prompt reads /dev/tty, not stdin, so test the terminal itself. */ int tty = open("/dev/tty", O_RDWR); if (tty < 0) return input_failure("Password prompt requires a terminal; use --password or stdin mode"); close(tty); password = PROMPT_PASSWORD("Database password: "); if (password && *password && prompt_limit_failure(password)) return 2; } if (!password || !*password || strlen(password) >= sizeof(login_password)) return input_failure("Invalid password length"); strcpy(login_user, username); strcpy(login_password, password); } if (!server || !*server || !*database || strlen(server) + strlen(database) + 2 > sizeof(database_name)) return input_failure("Invalid server alias or database name"); snprintf(database_name, sizeof(database_name), "%s@%s", database, server); EXEC SQL CONNECT TO :database_name USER :login_user USING :login_password; memset(login_password, 0, sizeof(login_password)); if (sqlca.sqlcode < 0) return sql_failure("connect"); /* systables tabid 1 exists in every database on every version, unlike * sysdual, so --database can name any database. */ EXEC SQL SELECT CURRENT::CHAR(40), DBINFO('dbhostname') INTO :current_value, :connected_host FROM systables WHERE tabid = 1; if (sqlca.sqlcode != 0) return sql_failure("query"); /* Version metadata is optional and does not determine login success. */ EXEC SQL SELECT DBINFO('version', 'full') INTO :server_version FROM systables WHERE tabid = 1; if (sqlca.sqlcode != 0) server_version[0] = '\0'; /* ESQL/C blank-pads fixed char host variables; trim for both formats. */ trim_right(current_value); trim_right(connected_host); trim_right(server_version); if (output_raw) { /* CURRENT HOST VERSION; host has no spaces, version may. */ raw_string(current_value); printf(" "); raw_string(connected_host); printf(" "); raw_string(server_version); putchar('\n'); EXEC SQL DISCONNECT CURRENT; return 0; } printf("{\"success\":true,\"sqlcode\":0,\"current\":"); json_string(current_value); printf(",\"host\":"); json_string(connected_host); printf(",\"version\":"); json_string(server_version); puts("}"); EXEC SQL DISCONNECT CURRENT; return 0; }