/*****************************************************************************/ /* ProxyVerify.c (Draw in a deep breath, and ...) this module implements a relatively simple, pragmatic mechanism that allows a proxy server to authorize a request locally, then convey that authorized username to a reverse-proxied server using a standard HTTP "Authorization: basic ..." request field, while keeping the original password private, and providing an HTTP-based mechanism for the proxied-to server to verify that the request is indeed originating from the proxy server. It has been specifically developed to support a site (UMA) with these requirements. It may-or-may-not find more general usage! As there is no way to predetermine which process will receive a verification request when using multiple server instances the records must be capable of being stored in a global section shared memory. For a single instance the same data structures are supported in local memory. FUNCTIONAL DESCRIPTION ---------------------- 1) The proxy server subjects the reverse-proxy path to authorization. The client is required to provide credentials with the request. These credentials are assessed by the proxy server and the request allowed to procede or rejected and the credentials rerequested. 2) The username from the credentials is used to build a new, base-64 encoded, 'basic' authorization string (:) for use in the request header sent to the proxied-to server. The password component is derived from an MD5 hash of the username, a binary counter and binary time components. It is UNIQUE FOR EVERY REQUEST and is OPAQUE TO THE PROXIED-TO SERVER. The original password is not transmitted. This base-64 encoded authorization string is stored by the proxy server as a record for later verification. If for some reason it cannot be generated or stored (e.g. records all in use) the calling routine should abort the request and report an error (by checking whether 'tkptr->VerifyRecordPtr' has remained NULL). 3) The proxied-to server receives the request and decodes the "Authorization: basic ..." field sent with it, if required. From that the original username can be parsed. If the proxied-to server wishes to verify that request is what it purports to be - an already authorized, proxied request - it can request the proxy server to do that by sending a standard HTTP GET request containing the path prefix '/httpd/-/verify/' followed by the base-64 encoded string supplied with the "Authorization: basic ..." field. NOTE: the verification string contains the username as well as a hash derived in part from using that username. The username could not be substituted without changing the encoded string. Using the entire authorization string verifies the username as well as the request. 4) The proxy server receives a request to verify the included authorization string. It searches through the verification records looking for one containing that string. If it finds it and that request has not been previously verified returns a 200 (success) response. If it cannot find it a 404 (not found) response. If has been previously verified a 403 (forbidden) response. 5) The proxied-to server assesses the response and continues or aborts the request processing. 6) As the original request is rundown on the proxy server it clears the corresponding verification record. CONFIGURATION ------------- The facility is enabled by setting WASD_CONFIG_GLOBAL [ProxyVerifyRecordMax] to be non-zero. Specific paths must then be SET in WASD_CONFIG_MAP to have reverse-proxy verification performed. pass /httpd/-/verify/* redirect /proxied-to-server/* /http://proxied.to.server/* set http://proxied.to.server/* proxy=reverse=verify pass http://proxied.to.server/* Of course the reverse-proxy path must also be appropriately authorized in WASD_CONFIG_AUTH. ["realm description"=realm=type] http://proxied.to.server/* r+w DEVELOPMENT ----------- For development purposes the proxied-to server can make a request /httpd/-/verify/<3-digit-code> and receive the response appropriate to that HTTP status code without there being an actual request under verification. That is, using "/httpd/-/verify/200" returns a success response, "/httpd/-/verify/404" a not found response, "/httpd/-/verify/501" a disabled response, etc. VERSION HISTORY --------------- 22-MAY-2007 JPP bugfix; ProxyVerifyGblSecInit() 20-NOV-2003 MGD initial */ /*****************************************************************************/ #ifdef WASD_VMS_V7 #undef _VMS__V6__SOURCE #define _VMS__V6__SOURCE #undef __VMS_VER #define __VMS_VER 70000000 #undef __CRTL_VER #define __CRTL_VER 70000000 #endif /* standard C header files */ #include #include /* VMS related header files */ #include #include #include #include /* application related header files */ #include "wasd.h" #define WASD_MODULE "PROXYVERIFY" /******************/ /* global storage */ /******************/ int ProxyVerifyCurrentCount, ProxyVerifyGblSecPages, ProxyVerifyGblSecSize, ProxyVerifyRecordMax, ProxyVerifyRecordSize = sizeof(PROXYVERIFY_RECORD); PROXYVERIFY_GBLSEC *ProxyVerifyGblSecPtr; /********************/ /* external storage */ /********************/ extern BOOL HttpdServerStartup; extern int GblPageCount, GblSectionCount, InstanceNodeConfig, InstanceEnvNumber, OpcomMessages, ProxyVerifyGblSecVersion; extern unsigned long GblSecPrvMask[]; extern CONFIG_STRUCT Config; extern HTTPD_PROCESS HttpdProcess; extern PROXY_ACCOUNTING_STRUCT *ProxyAccountingPtr; extern SYS_INFO SysInfo; extern WATCH_STRUCT Watch; /*****************************************************************************/ /* If called during startup initialize (if necessary) the global section and then ensure that any uncleared records for this instance remaining from any non-clean previous shutdown are cleared. If during a clean shutdown just clear any remaining used records. */ ProxyVerifyInit () { int idx, RecordCount; PROXYVERIFY_RECORD *pvrptr, *RecordPoolPtr; /*********/ /* begin */ /*********/ if (WATCH_MODULE(WATCH_MOD_PROXY)) WatchThis (WATCHALL, WATCH_MOD_PROXY, "ProxyVerifyInit()"); if (HttpdServerStartup) { ProxyVerifyRecordMax = Config.cfProxy.VerifyRecordMax; if (!ProxyVerifyRecordMax) return; if (ProxyVerifyRecordMax < PROXY_VERIFY_DEFAULT_RECORD_MAX) ProxyVerifyRecordMax = PROXY_VERIFY_DEFAULT_RECORD_MAX; ProxyVerifyGblSecInit (); } if (!ProxyVerifyGblSecPtr) return; InstanceMutexLock (INSTANCE_MUTEX_PROXY_VERIFY); RecordCount = ProxyVerifyGblSecPtr->RecordCount; RecordPoolPtr = ProxyVerifyGblSecPtr->RecordPool; for (idx = 0; idx < RecordCount; idx++) { pvrptr = &RecordPoolPtr[idx]; if (!pvrptr->AuthorizationStringLength) continue; if (strcmp (pvrptr->HttpdPrcNam, HttpdProcess.PrcNam)) continue; memset (pvrptr, 0, sizeof(PROXYVERIFY_RECORD)); } InstanceMutexUnLock (INSTANCE_MUTEX_PROXY_VERIFY); } /*****************************************************************************/ /* Create a base-64 encoded string containing the original, authenticated remote username, along with an MD5 hash of a concatenation of that username, and a binary counter and time compononents. This base-64 encoded string is in a format suitable for use as 'basic' HTTP authorization. The PROXY.C module will check if the path is set for proxy verify, and that 'tkptr->VerifyRecordPtr' in non-NULL and rebuilds an "Authorization: basic ..." field using it if it does. */ ProxyVerifyRecordSet (PROXY_TASK *tkptr) { /* do NOT make this greater than 31! */ #define PROXY_VERIFY_PWD_LENGTH 31 static unsigned long VerifyTicker; int idx, RecordCount, UserNamePlusLength, UserNameLength; unsigned long Time64[2]; char *cptr, *sptr, *zptr; char UserNamePlus [AUTH_MAX_USERNAME_LENGTH+12], UserNamePwd [AUTH_MAX_USERNAME_LENGTH+32+1], UserNamePwdBase64 [PROXY_VERIFY_MAX_AUTH_LENGTH+1]; REQUEST_STRUCT *rqptr; PROXYVERIFY_RECORD *pvrptr, *RecordPoolPtr; /*********/ /* begin */ /*********/ if (WATCHMOD (tkptr, WATCH_MOD_PROXY)) WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY, "ProxyVerifyRecordSet() !&Z", tkptr->RequestPtr ? tkptr->RequestPtr->RemoteUser : ""); if (!ProxyVerifyGblSecPtr) return; if (!(rqptr = tkptr->RequestPtr)) return; /* only generate such if the request has actually been authenticated */ if (VMSnok(rqptr->rqAuth.FinalStatus) || !rqptr->RemoteUser[0]) return; /* 'HttpdTime64' has a granularity of only one second, use this one */ sys$gettim (&Time64); /* first use is set to something less determinate */ if (!VerifyTicker) VerifyTicker = Time64[0]; VerifyTicker += 11; zptr = (sptr = UserNamePlus) + AUTH_MAX_USERNAME_LENGTH; for (cptr = rqptr->RemoteUser; *cptr && sptr < zptr; *sptr++ = *cptr++); SET4(sptr,VerifyTicker); sptr += 4; SET4(sptr,Time64[0]); sptr += 4; SET4(sptr,Time64[1]); sptr += 4; UserNamePlusLength = sptr - UserNamePlus; zptr = (sptr = UserNamePwd) + AUTH_MAX_USERNAME_LENGTH; for (cptr = rqptr->RemoteUser; *cptr && sptr < zptr; *sptr++ = *cptr++); UserNameLength = sptr - UserNamePwd; *sptr++ = ':'; Md5HexString (UserNamePlus, UserNamePlusLength, UserNamePwd+UserNameLength+1); /* trim the MD5 hex digest back to it's maximum length (31) */ UserNamePwd[UserNameLength+1+PROXY_VERIFY_PWD_LENGTH] = '\0'; BasicPrintableEncode (UserNamePwd, UserNamePwdBase64, sizeof(UserNamePwdBase64)); if (WATCHMOD (rqptr, WATCH_MOD_PROXY)) WatchDataFormatted ("!&Z !&Z\n", UserNamePwd, UserNamePwdBase64); /* search for a free record to store this in */ InstanceMutexLock (INSTANCE_MUTEX_PROXY_VERIFY); RecordCount = ProxyVerifyGblSecPtr->RecordCount; RecordPoolPtr = ProxyVerifyGblSecPtr->RecordPool; for (idx = 0; idx < RecordCount; idx++) { pvrptr = &RecordPoolPtr[idx]; if (pvrptr->AuthorizationStringLength) continue; break; } if (idx < ProxyVerifyRecordMax) { if (idx >= RecordCount) pvrptr = &RecordPoolPtr[ProxyVerifyGblSecPtr->RecordCount++]; pvrptr->AuthorizationStringLength = strlen(UserNamePwdBase64); strcpy (pvrptr->AuthorizationString, UserNamePwdBase64); strcpy (pvrptr->HttpdPrcNam, HttpdProcess.PrcNam); strcpy (pvrptr->RemoteUser, rqptr->RemoteUser); strcpy (pvrptr->RealmName, rqptr->rqAuth.RealmPtr); pvrptr->SourceRealm = rqptr->rqAuth.SourceRealm; /* note the record in the task structure */ tkptr->VerifyRecordPtr = pvrptr; } InstanceMutexUnLock (INSTANCE_MUTEX_PROXY_VERIFY); InstanceMutexLock (INSTANCE_MUTEX_HTTPD); ProxyAccountingPtr->VerifySetRecordCount++; if (idx < ProxyVerifyRecordMax) ProxyAccountingPtr->VerifyCurrentCount++; else ProxyAccountingPtr->VerifyFullCount++; InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD); if (idx < ProxyVerifyRecordMax) return; ErrorNoticed (rqptr, 0, "record space exhausted", FI_LI); } /*****************************************************************************/ /* Clear the pointed-to record as unused. Called by ProxyEnd() as part of a proxied request rundown. */ ProxyVerifyRecordReset (PROXY_TASK *tkptr) { /*********/ /* begin */ /*********/ if (WATCHMOD (tkptr, WATCH_MOD_PROXY)) WatchThis (WATCHITM(tkptr), WATCH_MOD_PROXY, "ProxyVerifyRecordReset() !&Z", tkptr->RequestPtr ? tkptr->RequestPtr->RemoteUser : ""); InstanceMutexLock (INSTANCE_MUTEX_PROXY_VERIFY); memset (tkptr->VerifyRecordPtr, 0, sizeof(PROXYVERIFY_RECORD)); InstanceMutexUnLock (INSTANCE_MUTEX_PROXY_VERIFY); InstanceMutexLock (INSTANCE_MUTEX_HTTPD); if (ProxyAccountingPtr->VerifyCurrentCount) ProxyAccountingPtr->VerifyCurrentCount--; InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD); tkptr->VerifyRecordPtr = NULL; } /*****************************************************************************/ /* This function performs the '/httpd/-/verify/' functionality. The client appends the "Authorization: basic ..." base-64 encoded string to the above path and makes a GET request using it. The base-64 encoded string is extracted from the path then the array of records is searched for one containing this string. An appropriate HTTP response is generated and returned to the client. */ ProxyVerifyRequest ( REQUEST_STRUCT *rqptr, REQUEST_AST NextTaskFunction ) { int idx, RecordCount, StatusCode, StringLength, VerifyAttemptCount; unsigned long First4; char *cptr, *sptr; char UserNamePwd [AUTH_MAX_USERNAME_LENGTH+1+32+1]; PROXYVERIFY_RECORD *pvrptr, *RecordPoolPtr; /*********/ /* begin */ /*********/ if (WATCHMOD (rqptr, WATCH_MOD_PROXY)) WatchThis (WATCHITM(rqptr), WATCH_MOD_PROXY, "ProxyVerifyRequest()"); if (!ProxyVerifyGblSecPtr) { /* global section not initialized, facility not available! */ ResponseHeader (rqptr, 501, "text/plain", 14, NULL, NULL); NetWrite (rqptr, NextTaskFunction, "501 disabled!\n", 14); return; } /* do not cache any of these responses */ rqptr->rqResponse.PreExpired = true; cptr = rqptr->rqHeader.PathInfoPtr + sizeof(HTTPD_VERIFY)-1; StringLength = strlen(cptr); if (StringLength == 3) { /**************************/ /* for developer purposes */ /**************************/ StatusCode = atoi(cptr); switch (StatusCode) { case 200 : ResponseHeader (rqptr, 200, "text/plain", 7, NULL, NULL); NetWrite (rqptr, NextTaskFunction, "200 ok\n", 7); return; case 403 : ResponseHeader (rqptr, 403, "text/plain", 14, NULL, NULL); NetWrite (rqptr, NextTaskFunction, "403 forbidden\n", 14); return; case 404 : ResponseHeader (rqptr, 404, "text/plain", 12, NULL, NULL); NetWrite (rqptr, NextTaskFunction, "404 unknown\n", 12); return; case 501 : ResponseHeader (rqptr, 501, "text/plain", 14, NULL, NULL); NetWrite (rqptr, NextTaskFunction, "501 disabled!\n", 14); return; default : rqptr->rqResponse.HttpStatus = StatusCode; ErrorGeneral (rqptr, rqptr->rqHeader.PathInfoPtr, FI_LI); SysDclAst (NextTaskFunction, rqptr); return; } } First4 = *(ULONGPTR)cptr; InstanceMutexLock (INSTANCE_MUTEX_PROXY_VERIFY); RecordCount = ProxyVerifyGblSecPtr->RecordCount; RecordPoolPtr = ProxyVerifyGblSecPtr->RecordPool; for (idx = 0; idx < RecordCount; idx++) { pvrptr = &RecordPoolPtr[idx]; if (!pvrptr->AuthorizationStringLength) continue; if (pvrptr->AuthorizationStringLength != StringLength) continue; if (*(ULONGPTR)pvrptr->AuthorizationString != First4) continue; if (strcmp (pvrptr->AuthorizationString, cptr)) continue; /*********/ /* found */ /*********/ if (WATCHPNT(rqptr) && (WATCH_CATEGORY(WATCH_AUTH) || WATCH_CATEGORY(WATCH_PROXY))) WatchThis (WATCHITM(rqptr), WATCH_CATEGORY(WATCH_AUTH) ? WATCH_AUTH : WATCH_PROXY, "VERIFY instance:!AZ user:!AZ realm:!AZ!AZ", pvrptr->HttpdPrcNam, pvrptr->RemoteUser, pvrptr->RealmName, AuthSourceString (pvrptr->RealmName, pvrptr->SourceRealm)); if (!pvrptr->VerifyAttemptCount++) { /***************************/ /* successful verification */ /***************************/ InstanceMutexUnLock (INSTANCE_MUTEX_PROXY_VERIFY); InstanceMutexLock (INSTANCE_MUTEX_HTTPD); ProxyAccountingPtr->VerifyFindRecordCount++; ProxyAccountingPtr->Verify200Count++; InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD); ResponseHeader (rqptr, 200, "text/plain", 7, NULL, NULL); NetWrite (rqptr, NextTaskFunction, "200 ok\n", 7); return; } /********************************/ /* uh-oh ... multiple attempts! */ /********************************/ VerifyAttemptCount = pvrptr->VerifyAttemptCount; InstanceMutexUnLock (INSTANCE_MUTEX_PROXY_VERIFY); InstanceMutexLock (INSTANCE_MUTEX_HTTPD); ProxyAccountingPtr->VerifyFindRecordCount++; ProxyAccountingPtr->Verify403Count++; InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD); BasicPrintableDecode (cptr, UserNamePwd, sizeof(UserNamePwd)); for (sptr = UserNamePwd; *sptr && *sptr != ':'; sptr++); *sptr = '\0'; FaoToStdout ("%HTTPD-W-PROXYVERIFY, !20%D, !AZ (!AZ) !AZ !UL attempts\n", 0, cptr, UserNamePwd, rqptr->ClientPtr->Lookup.HostName, VerifyAttemptCount); if (OpcomMessages & OPCOM_AUTHORIZATION) FaoToOpcom ("%HTTPD-W-PROXYVERIFY, !AZ (!AZ) !AZ !UL attempts", cptr, UserNamePwd, rqptr->ClientPtr->Lookup.HostName, VerifyAttemptCount); ResponseHeader (rqptr, 403, "text/plain", 14, NULL, NULL); NetWrite (rqptr, NextTaskFunction, "403 forbidden\n", 14); return; } /*************/ /* not found */ /*************/ InstanceMutexUnLock (INSTANCE_MUTEX_PROXY_VERIFY); InstanceMutexLock (INSTANCE_MUTEX_HTTPD); ProxyAccountingPtr->VerifyFindRecordCount++; ProxyAccountingPtr->Verify404Count++; InstanceMutexUnLock (INSTANCE_MUTEX_HTTPD); BasicPrintableDecode (cptr, UserNamePwd, sizeof(UserNamePwd)); for (sptr = UserNamePwd; *sptr && *sptr != ':'; sptr++); *sptr = '\0'; FaoToStdout ("%HTTPD-W-PROXYVERIFY, !20%D, !AZ (!AZ) !AZ unknown\n", 0, cptr, UserNamePwd, rqptr->ClientPtr->Lookup.HostName); if (OpcomMessages & OPCOM_AUTHORIZATION) FaoToOpcom ("%HTTPD-W-PROXYVERIFY, !AZ (!AZ) !AZ unknown", cptr, UserNamePwd, rqptr->ClientPtr->Lookup.HostName); ResponseHeader (rqptr, 404, "text/plain", 12, NULL, NULL); NetWrite (rqptr, NextTaskFunction, "404 unknown\n", 12); return; } /*****************************************************************************/ /* If only one instance can execute (from configuration) then allocate a block of process-local dynamic memory and point to that as the cache. If multiple instances create and map a global section and point to that. */ ProxyVerifyGblSecInit () { static char GblSecReport [] = "%HTTPD-I-PROXYVERIFY, for !UL records in !AZ of !UL page(let)s\n"; /* global, allocate space, system, in page file, writable */ static int CreFlags = SEC$M_GBL | SEC$M_EXPREG | SEC$M_SYSGBL | SEC$M_PAGFIL | SEC$M_WRT; static int DelFlags = SEC$M_SYSGBL; /* system & owner full access, group and world no access */ static unsigned long ProtectionMask = 0xff00; /* it is recommended to map into any virtual address in the region (P0) */ static unsigned long InAddr [2] = { 0x200, 0x200 }; int attempt, status, BaseGblSecPages, VerifyRecordPoolSize, PageCount, SetPrvStatus; short ShortLength; unsigned long RetAddr [2]; char GblSecName [32]; $DESCRIPTOR (GblSecNameDsc, GblSecName); PROXYVERIFY_GBLSEC *gsptr; /*********/ /* begin */ /*********/ if (WATCH_MODULE(WATCH_MOD_PROXY)) WatchThis (WATCHALL, WATCH_MOD_PROXY, "ProxyVerifyGblSecInit()"); VerifyRecordPoolSize = ProxyVerifyRecordSize * ProxyVerifyRecordMax; ProxyVerifyGblSecSize = sizeof(PROXYVERIFY_GBLSEC) + VerifyRecordPoolSize; ProxyVerifyGblSecPages = ProxyVerifyGblSecSize / 512; if (ProxyVerifyGblSecSize & 0x1ff) ProxyVerifyGblSecPages++; if (InstanceNodeConfig <= 1) { /* no need for a global section, just use process-local storage */ ProxyVerifyGblSecPtr = (PROXYVERIFY_GBLSEC*)VmGet (ProxyVerifyGblSecPages * 512); sys$gettim (&ProxyVerifyGblSecPtr->SinceTime64); FaoToStdout (GblSecReport, ProxyVerifyRecordMax, "local storage", ProxyVerifyGblSecPages); return (SS$_CREATED); } FaoToBuffer (GblSecName, sizeof(GblSecName), &ShortLength, GBLSEC_NAME_FAO, HTTPD_NAME, PROXYVERIFY_GBLSEC_VERSION_NUMBER, InstanceEnvNumber, "PROXYVERIFY"); GblSecNameDsc.dsc$w_length = ShortLength; if VMSnok ((SetPrvStatus = sys$setprv (1, &GblSecPrvMask, 0, 0))) ErrorExitVmsStatus (SetPrvStatus, "sys$setprv()", FI_LI); for (attempt = 1; attempt <= 2; attempt++) { /* create and/or map the specified global section */ sys$setprv (1, &GblSecPrvMask, 0, 0); status = sys$crmpsc (&InAddr, &RetAddr, 0, CreFlags, &GblSecNameDsc, 0, 0, 0, ProxyVerifyGblSecPages, 0, ProtectionMask, ProxyVerifyGblSecPages); sys$setprv (0, &GblSecPrvMask, 0, 0); if (WATCH_MODULE(WATCH_MOD_PROXY)) WatchThis (WATCHALL, WATCH_MOD_PROXY, "sys$crmpsc() !&S begin:!UL end:!UL", status, RetAddr[0], RetAddr[1]); PageCount = (RetAddr[1]+1) - RetAddr[0] >> 9; ProxyVerifyGblSecPtr = gsptr = (PROXYVERIFY_GBLSEC*)RetAddr[0]; ProxyVerifyGblSecPages = PageCount; if (VMSnok (status) || status == SS$_CREATED) break; /* section already exists, break if 'same size' and version! */ if (gsptr->GblSecVersion && gsptr->GblSecVersion == ProxyVerifyGblSecVersion && gsptr->GblSecLength == ProxyVerifyGblSecSize) break; /* delete the current global section, have one more attempt */ sys$setprv (1, &GblSecPrvMask, 0, 0); status = sys$dgblsc (DelFlags, &GblSecNameDsc, 0); sys$setprv (0, &GblSecPrvMask, 0, 0); status = SS$_IDMISMATCH; } if (VMSnok (status)) { /* must have this global section! */ char String [256]; FaoToBuffer (String, sizeof(String), NULL, "1 global section, !UL global pages", ProxyVerifyGblSecPages); ErrorExitVmsStatus (status, String, FI_LI); } if (WATCH_MODULE(WATCH_MOD_PROXY)) WatchThis (WATCHALL, WATCH_MOD_PROXY, "GBLSEC \"!AZ\" page(let)s:!UL !&S", GblSecName, PageCount, status); FaoToStdout (GblSecReport, ProxyVerifyRecordMax, status == SS$_CREATED ? "a new global section" : "an existing global section", ProxyVerifyGblSecPages); if (status == SS$_CREATED) { /* first time it's been mapped */ memset (gsptr, 0, PageCount * 512); gsptr->GblSecVersion = ProxyVerifyGblSecVersion; gsptr->GblSecLength = ProxyVerifyGblSecSize; sys$gettim (&ProxyVerifyGblSecPtr->SinceTime64); } GblSectionCount++; GblPageCount += PageCount; return (status); } /*****************************************************************************/