001: // ========================================================================
002: // Authors : Van den Broeke Iris, Deville Daniel, Dubois Roger, Greg Wilkins
003: // Copyright (c) 2001 Deville Daniel. All rights reserved.
004: // Permission to use, copy, modify and distribute this software
005: // for non-commercial or commercial purposes and without fee is
006: // hereby granted provided that this copyright notice appears in
007: // all copies.
008: // ========================================================================
009:
010: package org.mortbay.jetty.security;
011:
012: import java.io.BufferedReader;
013: import java.io.IOException;
014: import java.io.InputStreamReader;
015: import java.net.URI;
016: import java.net.URL;
017: import java.security.Principal;
018: import java.util.ArrayList;
019: import java.util.HashMap;
020: import java.util.HashSet;
021: import java.util.Map;
022: import java.util.StringTokenizer;
023:
024: import javax.servlet.ServletException;
025: import javax.servlet.http.HttpServletRequest;
026: import javax.servlet.http.HttpServletResponse;
027:
028: import org.mortbay.jetty.Handler;
029: import org.mortbay.jetty.HttpConnection;
030: import org.mortbay.jetty.HttpFields;
031: import org.mortbay.jetty.HttpHeaders;
032: import org.mortbay.jetty.Request;
033: import org.mortbay.jetty.Response;
034: import org.mortbay.jetty.handler.AbstractHandler;
035: import org.mortbay.jetty.handler.ContextHandler;
036: import org.mortbay.jetty.handler.HandlerWrapper;
037: import org.mortbay.log.Log;
038: import org.mortbay.log.Logger;
039: import org.mortbay.resource.Resource;
040: import org.mortbay.util.StringUtil;
041: import org.mortbay.util.URIUtil;
042:
043: /* ------------------------------------------------------------ */
044: /**
045: * Handler to authenticate access using the Apache's .htaccess files.
046: *
047: * @author Van den Broeke Iris
048: * @author Deville Daniel
049: * @author Dubois Roger
050: * @author Greg Wilkins
051: * @author Konstantin Metlov
052: *
053: */
054: public class HTAccessHandler extends SecurityHandler {
055: private Handler protegee;
056: private static Logger log = Log.getLogger(HTAccessHandler.class
057: .getName());
058:
059: String _default = null;
060: String _accessFile = ".htaccess";
061:
062: transient HashMap _htCache = new HashMap();
063:
064: /* ------------------------------------------------------------ */
065: /**
066: * {@inheritDoc}
067: *
068: * @see org.mortbay.jetty.Handler#handle(java.lang.String,
069: * javax.servlet.http.HttpServletRequest,
070: * javax.servlet.http.HttpServletResponse, int)
071: */
072: public void handle(String target, HttpServletRequest request,
073: HttpServletResponse response, int dispatch)
074: throws IOException, ServletException {
075: Request base_request = (request instanceof Request) ? (Request) request
076: : HttpConnection.getCurrentConnection().getRequest();
077: Response base_response = (response instanceof Response) ? (Response) response
078: : HttpConnection.getCurrentConnection().getResponse();
079:
080: String pathInContext = target;
081:
082: String user = null;
083: String password = null;
084: boolean IPValid = true;
085:
086: if (log.isDebugEnabled())
087: log.debug("HTAccessHandler pathInContext=" + pathInContext,
088: null, null);
089:
090: String credentials = request
091: .getHeader(HttpHeaders.AUTHORIZATION);
092:
093: if (credentials != null) {
094: credentials = credentials.substring(credentials
095: .indexOf(' ') + 1);
096: credentials = B64Code.decode(credentials,
097: StringUtil.__ISO_8859_1);
098: int i = credentials.indexOf(':');
099: user = credentials.substring(0, i);
100: password = credentials.substring(i + 1);
101:
102: if (log.isDebugEnabled())
103: log.debug("User="
104: + user
105: + ", password="
106: + "******************************".substring(0,
107: password.length()), null, null);
108: }
109:
110: HTAccess ht = null;
111:
112: try {
113: Resource resource = null;
114: String directory = pathInContext.endsWith("/") ? pathInContext
115: : URIUtil.parentPath(pathInContext);
116:
117: // Look for htAccess resource
118: while (directory != null) {
119: String htPath = directory + _accessFile;
120: resource = ((ContextHandler) getProtegee())
121: .getResource(htPath);
122: if (log.isDebugEnabled())
123: log.debug("directory=" + directory + " resource="
124: + resource, null, null);
125:
126: if (resource != null && resource.exists()
127: && !resource.isDirectory())
128: break;
129: resource = null;
130: directory = URIUtil.parentPath(directory);
131: }
132:
133: boolean haveHtAccess = true;
134:
135: // Try default directory
136: if (resource == null && _default != null) {
137: resource = Resource.newResource(_default);
138: if (!resource.exists() || resource.isDirectory())
139: haveHtAccess = false;
140: }
141: if (resource == null)
142: haveHtAccess = false;
143:
144: // prevent access to htaccess files
145: if (pathInContext.endsWith(_accessFile)
146: // extra security
147: || pathInContext.endsWith(_accessFile + "~")
148: || pathInContext.endsWith(_accessFile + ".bak")) {
149: response.sendError(HttpServletResponse.SC_FORBIDDEN);
150: base_request.setHandled(true);
151: return;
152: }
153:
154: if (haveHtAccess) {
155: if (log.isDebugEnabled())
156: log.debug("HTACCESS=" + resource, null, null);
157:
158: ht = (HTAccess) _htCache.get(resource);
159: if (ht == null
160: || ht.getLastModified() != resource
161: .lastModified()) {
162: ht = new HTAccess(resource);
163: _htCache.put(resource, ht);
164: if (log.isDebugEnabled())
165: log.debug("HTCache loaded " + ht, null, null);
166: }
167:
168: // See if there is a config problem
169: if (ht.isForbidden()) {
170: log.warn("Mis-configured htaccess: " + ht, null,
171: null);
172: response
173: .sendError(HttpServletResponse.SC_FORBIDDEN);
174: base_request.setHandled(true);
175: return;
176: }
177:
178: // first see if we need to handle based on method type
179: Map methods = ht.getMethods();
180: if (methods.size() > 0
181: && !methods.containsKey(request.getMethod()))
182: return; // Nothing to check
183:
184: // Check the accesss
185: int satisfy = ht.getSatisfy();
186:
187: // second check IP address
188: IPValid = ht.checkAccess("", request.getRemoteAddr());
189: if (log.isDebugEnabled())
190: log.debug("IPValid = " + IPValid, null, null);
191:
192: // If IP is correct and satify is ANY then access is allowed
193: if (IPValid == true && satisfy == HTAccess.ANY)
194: return;
195:
196: // If IP is NOT correct and satify is ALL then access is
197: // forbidden
198: if (IPValid == false && satisfy == HTAccess.ALL) {
199: response
200: .sendError(HttpServletResponse.SC_FORBIDDEN);
201: base_request.setHandled(true);
202: return;
203: }
204:
205: // set required page
206: if (!ht.checkAuth(user, password, getUserRealm(),
207: base_request)) {
208: log.debug("Auth Failed", null, null);
209: response.setHeader(HttpHeaders.WWW_AUTHENTICATE,
210: "basic realm=" + ht.getName());
211: response
212: .sendError(HttpServletResponse.SC_UNAUTHORIZED);
213: base_response.complete();
214: base_request.setHandled(true);
215: return;
216: }
217:
218: // set user
219: if (user != null) {
220: base_request.setAuthType(Constraint.__BASIC_AUTH);
221: base_request.setUserPrincipal(getUserRealm()
222: .getPrincipal(user));
223: }
224: }
225:
226: if (getHandler() != null) {
227: getHandler()
228: .handle(target, request, response, dispatch);
229: }
230:
231: } catch (Exception ex) {
232: log.warn("Exception", ex);
233: if (ht != null) {
234: response
235: .sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
236: base_request.setHandled(true);
237: }
238: }
239: }
240:
241: /* ------------------------------------------------------------ */
242: /**
243: * set functions for the following .xml administration statements.
244: *
245: * <Call name="addHandler"> <Arg> <New
246: * class="org.mortbay.http.handler.HTAccessHandler"> <Set
247: * name="Default">./etc/htaccess</Set> <Set name="AccessFile">.htaccess</Set>
248: * </New> </Arg> </Call>
249: *
250: */
251: public void setDefault(String dir) {
252: _default = dir;
253: }
254:
255: /* ------------------------------------------------------------ */
256: public void setAccessFile(String anArg) {
257: if (anArg == null)
258: _accessFile = ".htaccess";
259: else
260: _accessFile = anArg;
261: }
262:
263: /* ------------------------------------------------------------ */
264: /* ------------------------------------------------------------ */
265: /* ------------------------------------------------------------ */
266: private static class HTAccess {
267: // private boolean _debug = false;
268: static final int ANY = 0;
269: static final int ALL = 1;
270: static final String USER = "user";
271: static final String GROUP = "group";
272: static final String VALID_USER = "valid-user";
273:
274: /* ------------------------------------------------------------ */
275: String _userFile;
276: Resource _userResource;
277: HashMap _users = null;
278: long _userModified;
279:
280: /* ------------------------------------------------------------ */
281: String _groupFile;
282: Resource _groupResource;
283: HashMap _groups = null;
284: long _groupModified;
285:
286: int _satisfy = 0;
287: String _type;
288: String _name;
289: HashMap _methods = new HashMap();
290: HashSet _requireEntities = new HashSet();
291: String _requireName;
292: int _order;
293: ArrayList _allowList = new ArrayList();
294: ArrayList _denyList = new ArrayList();
295: long _lastModified;
296: boolean _forbidden = false;
297:
298: /* ------------------------------------------------------------ */
299: public HTAccess(Resource resource) {
300: BufferedReader htin = null;
301: try {
302: htin = new BufferedReader(new InputStreamReader(
303: resource.getInputStream()));
304: parse(htin);
305: _lastModified = resource.lastModified();
306:
307: if (_userFile != null) {
308: _userResource = Resource.newResource(_userFile);
309: if (!_userResource.exists()) {
310: _forbidden = true;
311: log.warn("Could not find ht user file: "
312: + _userFile, null, null);
313: } else if (log.isDebugEnabled())
314: log.debug("user file: " + _userResource, null,
315: null);
316: }
317:
318: if (_groupFile != null) {
319: _groupResource = Resource.newResource(_groupFile);
320: if (!_groupResource.exists()) {
321: _forbidden = true;
322: log.warn("Could not find ht group file: "
323: + _groupResource, null, null);
324: } else if (log.isDebugEnabled())
325: log.debug("group file: " + _groupResource,
326: null, null);
327: }
328: } catch (IOException e) {
329: _forbidden = true;
330: log.warn("LogSupport.EXCEPTION", e);
331: }
332: }
333:
334: /* ------------------------------------------------------------ */
335: public boolean isForbidden() {
336: return _forbidden;
337: }
338:
339: /* ------------------------------------------------------------ */
340: public HashMap getMethods() {
341: return _methods;
342: }
343:
344: /* ------------------------------------------------------------ */
345: public long getLastModified() {
346: return _lastModified;
347: }
348:
349: /* ------------------------------------------------------------ */
350: public Resource getUserResource() {
351: return _userResource;
352: }
353:
354: /* ------------------------------------------------------------ */
355: public Resource getGroupResource() {
356: return _groupResource;
357: }
358:
359: /* ------------------------------------------------------------ */
360: public int getSatisfy() {
361: return (_satisfy);
362: }
363:
364: /* ------------------------------------------------------------ */
365: public String getName() {
366: return _name;
367: }
368:
369: /* ------------------------------------------------------------ */
370: public String getType() {
371: return _type;
372: }
373:
374: /* ------------------------------------------------------------ */
375: public boolean checkAccess(String host, String ip) {
376: String elm;
377: boolean alp = false;
378: boolean dep = false;
379:
380: // if no allows and no deny defined, then return true
381: if (_allowList.size() == 0 && _denyList.size() == 0)
382: return (true);
383:
384: // looping for allows
385: for (int i = 0; i < _allowList.size(); i++) {
386: elm = (String) _allowList.get(i);
387: if (elm.equals("all")) {
388: alp = true;
389: break;
390: } else {
391: char c = elm.charAt(0);
392: if (c >= '0' && c <= '9') {
393: // ip
394: if (ip.startsWith(elm)) {
395: alp = true;
396: break;
397: }
398: } else {
399: // hostname
400: if (host.endsWith(elm)) {
401: alp = true;
402: break;
403: }
404: }
405: }
406: }
407:
408: // looping for denies
409: for (int i = 0; i < _denyList.size(); i++) {
410: elm = (String) _denyList.get(i);
411: if (elm.equals("all")) {
412: dep = true;
413: break;
414: } else {
415: char c = elm.charAt(0);
416: if (c >= '0' && c <= '9') { // ip
417: if (ip.startsWith(elm)) {
418: dep = true;
419: break;
420: }
421: } else { // hostname
422: if (host.endsWith(elm)) {
423: dep = true;
424: break;
425: }
426: }
427: }
428: }
429:
430: if (_order < 0) // deny,allow
431: return !dep || alp;
432: // mutual failure == allow,deny
433: return alp && !dep;
434: }
435:
436: /* ------------------------------------------------------------ */
437: public boolean checkAuth(String user, String pass,
438: UserRealm realm, Request request) {
439: if (_requireName == null)
440: return true;
441:
442: // Authenticate with realm
443:
444: Principal principal = realm == null ? null : realm
445: .authenticate(user, pass, request);
446: if (principal == null) {
447: // Have to authenticate the user with the password file
448: String code = getUserCode(user);
449: String salt = code != null ? code.substring(0, 2)
450: : user;
451: String cred = (user != null && pass != null) ? UnixCrypt
452: .crypt(pass, salt)
453: : null;
454: if (code == null
455: || (code.equals("") && !pass.equals(""))
456: || !code.equals(cred))
457: return false;
458: }
459:
460: if (_requireName.equalsIgnoreCase(USER)) {
461: if (_requireEntities.contains(user))
462: return true;
463: } else if (_requireName.equalsIgnoreCase(GROUP)) {
464: ArrayList gps = getUserGroups(user);
465: if (gps != null)
466: for (int g = gps.size(); g-- > 0;)
467: if (_requireEntities.contains(gps.get(g)))
468: return true;
469: } else if (_requireName.equalsIgnoreCase(VALID_USER)) {
470: return true;
471: }
472:
473: return false;
474: }
475:
476: /* ------------------------------------------------------------ */
477: public boolean isAccessLimited() {
478: if (_allowList.size() > 0 || _denyList.size() > 0)
479: return true;
480: else
481: return false;
482: }
483:
484: /* ------------------------------------------------------------ */
485: public boolean isAuthLimited() {
486: if (_requireName != null)
487: return true;
488: else
489: return false;
490: }
491:
492: /* ------------------------------------------------------------ */
493: private String getUserCode(String user) {
494: if (_userResource == null)
495: return null;
496:
497: if (_users == null
498: || _userModified != _userResource.lastModified()) {
499: if (log.isDebugEnabled())
500: log.debug("LOAD " + _userResource, null, null);
501: _users = new HashMap();
502: BufferedReader ufin = null;
503: try {
504: ufin = new BufferedReader(new InputStreamReader(
505: _userResource.getInputStream()));
506: _userModified = _userResource.lastModified();
507: String line;
508: while ((line = ufin.readLine()) != null) {
509: line = line.trim();
510: if (line.startsWith("#"))
511: continue;
512: int spos = line.indexOf(':');
513: if (spos < 0)
514: continue;
515: String u = line.substring(0, spos).trim();
516: String p = line.substring(spos + 1).trim();
517: _users.put(u, p);
518: }
519: } catch (IOException e) {
520: log.warn("LogSupport.EXCEPTION", e);
521: } finally {
522: try {
523: if (ufin != null)
524: ufin.close();
525: } catch (IOException e2) {
526: log.warn("LogSupport.EXCEPTION", e2);
527: }
528: }
529: }
530:
531: return (String) _users.get(user);
532: }
533:
534: /* ------------------------------------------------------------ */
535: private ArrayList getUserGroups(String group) {
536: if (_groupResource == null)
537: return null;
538:
539: if (_groups == null
540: || _groupModified != _groupResource.lastModified()) {
541: if (log.isDebugEnabled())
542: log.debug("LOAD " + _groupResource, null, null);
543:
544: _groups = new HashMap();
545: BufferedReader ufin = null;
546: try {
547: ufin = new BufferedReader(new InputStreamReader(
548: _groupResource.getInputStream()));
549: _groupModified = _groupResource.lastModified();
550: String line;
551: while ((line = ufin.readLine()) != null) {
552: line = line.trim();
553: if (line.startsWith("#") || line.length() == 0)
554: continue;
555:
556: StringTokenizer tok = new StringTokenizer(line,
557: ": \t");
558:
559: if (!tok.hasMoreTokens())
560: continue;
561: String g = tok.nextToken();
562: if (!tok.hasMoreTokens())
563: continue;
564: while (tok.hasMoreTokens()) {
565: String u = tok.nextToken();
566: ArrayList gl = (ArrayList) _groups.get(u);
567: if (gl == null) {
568: gl = new ArrayList();
569: _groups.put(u, gl);
570: }
571: gl.add(g);
572: }
573: }
574: } catch (IOException e) {
575: log.warn("LogSupport.EXCEPTION", e);
576: } finally {
577: try {
578: if (ufin != null)
579: ufin.close();
580: } catch (IOException e2) {
581: log.warn("LogSupport.EXCEPTION", e2);
582: }
583: }
584: }
585:
586: return (ArrayList) _groups.get(group);
587: }
588:
589: /* ------------------------------------------------------------ */
590: public String toString() {
591: StringBuffer buf = new StringBuffer();
592:
593: buf.append("AuthUserFile=");
594: buf.append(_userFile);
595: buf.append(", AuthGroupFile=");
596: buf.append(_groupFile);
597: buf.append(", AuthName=");
598: buf.append(_name);
599: buf.append(", AuthType=");
600: buf.append(_type);
601: buf.append(", Methods=");
602: buf.append(_methods);
603: buf.append(", satisfy=");
604: buf.append(_satisfy);
605: if (_order < 0)
606: buf.append(", order=deny,allow");
607: else if (_order > 0)
608: buf.append(", order=allow,deny");
609: else
610: buf.append(", order=mutual-failure");
611:
612: buf.append(", Allow from=");
613: buf.append(_allowList);
614: buf.append(", deny from=");
615: buf.append(_denyList);
616: buf.append(", requireName=");
617: buf.append(_requireName);
618: buf.append(" ");
619: buf.append(_requireEntities);
620:
621: return buf.toString();
622: }
623:
624: /* ------------------------------------------------------------ */
625: private void parse(BufferedReader htin) throws IOException {
626: String line;
627: while ((line = htin.readLine()) != null) {
628: line = line.trim();
629: if (line.startsWith("#"))
630: continue;
631: else if (line.startsWith("AuthUserFile")) {
632: _userFile = line.substring(13).trim();
633: } else if (line.startsWith("AuthGroupFile")) {
634: _groupFile = line.substring(14).trim();
635: } else if (line.startsWith("AuthName")) {
636: _name = line.substring(8).trim();
637: } else if (line.startsWith("AuthType")) {
638: _type = line.substring(8).trim();
639: }
640: // else if (line.startsWith("<Limit")) {
641: else if (line.startsWith("<Limit")) {
642: int limit = line.length();
643: int endp = line.indexOf('>');
644: StringTokenizer tkns;
645:
646: if (endp < 0)
647: endp = limit;
648: tkns = new StringTokenizer(line.substring(6, endp));
649: while (tkns.hasMoreTokens()) {
650: _methods.put(tkns.nextToken(), Boolean.TRUE);
651: }
652:
653: while ((line = htin.readLine()) != null) {
654: line = line.trim();
655: if (line.startsWith("#"))
656: continue;
657: else if (line.startsWith("satisfy")) {
658: int pos1 = 7;
659: limit = line.length();
660: while ((pos1 < limit)
661: && (line.charAt(pos1) <= ' '))
662: pos1++;
663: int pos2 = pos1;
664: while ((pos2 < limit)
665: && (line.charAt(pos2) > ' '))
666: pos2++;
667: String l_string = line
668: .substring(pos1, pos2);
669: if (l_string.equals("all"))
670: _satisfy = 1;
671: else if (l_string.equals("any"))
672: _satisfy = 0;
673: } else if (line.startsWith("require")) {
674: int pos1 = 7;
675: limit = line.length();
676: while ((pos1 < limit)
677: && (line.charAt(pos1) <= ' '))
678: pos1++;
679: int pos2 = pos1;
680: while ((pos2 < limit)
681: && (line.charAt(pos2) > ' '))
682: pos2++;
683: _requireName = line.substring(pos1, pos2)
684: .toLowerCase();
685: if (USER.equals(_requireName))
686: _requireName = USER;
687: else if (GROUP.equals(_requireName))
688: _requireName = GROUP;
689: else if (VALID_USER.equals(_requireName))
690: _requireName = VALID_USER;
691:
692: pos1 = pos2 + 1;
693: if (pos1 < limit) {
694: while ((pos1 < limit)
695: && (line.charAt(pos1) <= ' '))
696: pos1++;
697:
698: tkns = new StringTokenizer(line
699: .substring(pos1));
700: while (tkns.hasMoreTokens()) {
701: _requireEntities.add(tkns
702: .nextToken());
703: }
704: }
705:
706: } else if (line.startsWith("order")) {
707: if (log.isDebugEnabled())
708: log
709: .debug("orderline=" + line
710: + "order=" + _order,
711: null, null);
712: if (line.indexOf("allow,deny") > 0) {
713: log.debug("==>allow+deny", null, null);
714: _order = 1;
715: } else if (line.indexOf("deny,allow") > 0) {
716: log.debug("==>deny,allow", null, null);
717: _order = -1;
718: } else if (line.indexOf("mutual-failure") > 0) {
719: log.debug("==>mutual", null, null);
720: _order = 0;
721: } else {
722: }
723: } else if (line.startsWith("allow from")) {
724: int pos1 = 10;
725: limit = line.length();
726: while ((pos1 < limit)
727: && (line.charAt(pos1) <= ' '))
728: pos1++;
729: if (log.isDebugEnabled())
730: log.debug("allow process:"
731: + line.substring(pos1), null,
732: null);
733: tkns = new StringTokenizer(line
734: .substring(pos1));
735: while (tkns.hasMoreTokens()) {
736: _allowList.add(tkns.nextToken());
737: }
738: } else if (line.startsWith("deny from")) {
739: int pos1 = 9;
740: limit = line.length();
741: while ((pos1 < limit)
742: && (line.charAt(pos1) <= ' '))
743: pos1++;
744: if (log.isDebugEnabled())
745: log.debug("deny process:"
746: + line.substring(pos1), null,
747: null);
748:
749: tkns = new StringTokenizer(line
750: .substring(pos1));
751: while (tkns.hasMoreTokens()) {
752: _denyList.add(tkns.nextToken());
753: }
754: } else if (line.startsWith("</Limit>"))
755: break;
756: }
757: }
758: }
759: }
760: }
761:
762: /**
763: * Getter for property protegee.
764: *
765: * @return Returns the protegee.
766: */
767: protected Handler getProtegee() {
768: return this .protegee;
769: }
770:
771: /**
772: * Setter for property protegee.
773: *
774: * @param protegee
775: * The protegee to set.
776: */
777: public void setProtegee(Handler protegee) {
778: this.protegee = protegee;
779: }
780:
781: }
|