I’d like to restrict access to a folder of controllers that are used for admin purposes only. I’ve tried a number of ways and not coming up with a solution. These controllers are behind password protection. But, I’d like to just remove it from view if someone happens to stumble upon the right directory. Can this be done? I’d rather not do it from htaccess. I have access to the apache config files, so I’d like to handle it there.
Does it have anything to do with the way Codeigniter routes? Or, am I just way off?
This what I’m using that doesn’t work
<Directory /var/www/application/controllers/folder/>
Order deny,allow
Deny from all
Allow from xxx.xxx.xxx.xxx
</Directory>
Due to the way we re-write urls to work with CI, you'd never match your Apache config because you're actually requesting index.php?{args}. If you want to filter, you have to do it in CI instead. Your options are a core controller or hooks.
A simple way to do it is to create a core controller that your admin/ area scripts extend, and check the IP there.
Something like this:
application/core/MY_Controller.php:
class MY_Controller extends CI_Controller
{
public function __construct()
{
parent::__construct();
$this->load->config('permitted_ips');
// check visitor IP against $config['ips'] array, redirect as needed
}
}
Then, in your 'sensitive' controllers, extend MY_Controller:
application/controllers/admin/seekrit.php
class Seekrit extends MY_Controller
{
public function __construct() {
parent::__construct();
/* at this point any invalid IP has been redirected */
}
}
Now, if you're already using a core controller for something else, just check $this->uri->segment() to see if they're in a restricted area before loading the allowed IP configuration file and checking / redirecting / dying or whatever else you need to do.
Also, there's no need to use a constructor in your admin controllers if you don't need one, as the parent will be constructed if one is not defined. Just be sure to call the parent if you define one.
You could also put the white list in a database, Redis, whatever.
Another way to do this would be by using hooks, specifically the pre_controller hook. By the time that hook is entered, all of the security and base classes have run. This would be appropriate if you wanted to protect some or all of your routes in a more granular fashion. There, you could define an array containing routes, such as:
$protected_routes = array(
'foo' => array(
'allow_ip' => '1.2.3.4',
'redirect_if_not' => site_url('goaway')
)
)
Then, in your hook class (or function) match the first segment (my example is just a function):
$CI = get_instance();
$CI->load-config('my_hook');
$protected_routes = $CI->config->item('protected_routes');
$segment = $CI->uri->segment(1); // foo
if (in_array($segment, $protected_routes)) {
// grab $protected_routes[$segment] and work with it
}
This has the advantage of not cluttering up your core controller as many people use that as a means of sharing code between methods. However, the hook will run on every request which means adding another two file loads to bootstrap.
I used the hook method on a large RESTful service to protect certain endpoints by requiring additional headers, and enforcing different kinds of rate limiting on others. Note, the code above is just an example of what could go in the hook, not how to set up the hook itself. Read the hooks section of the CI manual, it's extremely easy and straight forward.
Finally, if you really want to do it via .htaccess, you'll have to go by the request itself. The directory application/controllers/foo is never entered, the actual request is /foo/controller/method{args}, which causes CI to instantiate the foo/controller.php class. Remember, once re-written, the server sees index.php?....
To accomplish this, you can re-write based on the request URI pattern, something like this (have not tested, YMMV):
RewriteRule (^|/)foo(/|$) - [F,L]
Which can be used to redirect anyone accessing the virtual path to your protected controllers. This could be preferable as it prevents PHP from needing to handle it, but you lose the granularity of control over what happens when there is a match. Still, you could use something like the above re-write combined with a hook or core implementation if you have more than one sensitive area to protect.
Tim Post's idea above is similar to another method I either saw on this site or somewhere else. It took me awhile to get back to this issue, but at long last it's done.
As TheShiftExchange pointed out in the comments under my original question, .htaccess will not work for a Codeigniter project. Below is what I ultimately ended up with and seems to work well. It probably isn't 100% secure, but I really just wanted to remove these pages from being directly accessed. If someone were to manage to get to the page there is still a user/pass login screen.
New Config File in application/config
switch (ENVIRONMENT) //set in index.php
{
case 'development':
$config['admin_ips'] = array('XXX.XXX.XXX.XXX');
break;
case 'testing':
$config['admin_ips'] = array('XXX.XXX.XXX.XXX', 'XXX.XXX.XXX.XXX', 'XXX.XXX.XXX.XXX');
break;
case 'production':
$config['admin_ips'] = array('XXX.XXX.XXX.XXX', 'XXX.XXX.XXX.XXX', 'XXX.XXX.XXX.XXX');
break;
}
New Controller
class Admin_IP_Controller extends MY_Controller {
function __construct()
{
parent::__construct();
$this->load->config('admin_ips');
if (!in_array($this->input->ip_address(), $this->config->item('admin_ips')))
{
show_404();
}
}
}
Related
We have an old Yii application along with new Symfony one.
The basic idea is simple - I need to check if there is a route matching in Symfony application then it is cool, if not then bootstrap Yii application and try to handle the request with it.
The main idea to not instantiate AppKernel (and do not load autoload.php - since there is two different autoload.php for each project) before I am sure there is route matching.
Can I do it somehow?
We've done this before with legacy applications.
There are two approaches you can take.
Wrap your old application inside a symfony project (recommended).
Unfortunately this will indeed load the symfony front-controller and kernel. No way around that. You need to make sure that symfony can't handle the request and to do that the kernel needs to be booted up.
Use sub-directories and apache virtual hosts to load one application vs the other as needed.
Given option 1,
You can either create your own front controller that loads either symfony or yii by reading routes (from static files if using yml or xml, or annotations which will be more complex) OR EventListener (RequestListener) that listens to the HttpKernelInterface::MASTER_REQUEST and ensures that a route can be returned.
Creating your own front controller is the only way that you can make it not load the symfony kernel, but it will require you to write something that understands the routes in both frameworks (or at least symfony's) and hands off the request appropriately.
Event listener example:
public function onkernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
... Code to continue normally, or bootstrap yii and return a custom response... (Can include and ob_start, or make an http request, etc)
}
public function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['onKernelRequest']
];
}
As you see, the kernel needs to be booted to ensure symfony can't serve the route. Unless creating your own front controller (as stated above).
A third approach would be to create a fallback controller, which would load up a specified URL if no route was found within symfony. Although this approach is generally used for legacy projects that lack a framework and use page scripts instead of proper routes, and definitely requires the use/help of output buffering.
The EventListener approach gives you the opportunity to create a proper Request to hand off to yii, and using what is returned to create a Response as proper symfony object (can also use ob or other options).
Thank you.
This is an alternative to vpassapera's solution -http://stovepipe.systems/post/migrating-your-project-to-symfony
I have some hierarchical data which I have organized in the concrete5 filemanager. I'd like to know if it is possible to access the filemanager from outside the concrete5 website by other apps (something in the manner of an API).
This website made me hopeful that there could be an answer to this. Unfortunately, there was no followup tutorial.
http://c5hub.com/learning/building-rest-api-using-concrete5-part-1/
My second question is very much related: is it possible to do the same thing to access page information through the composer view?
Thanks
Okay, so I'm going to give some basic examples from what I think you need. Feel free to give any feedback if you need it to do some more specific.
First things first. Create a package (just because it looks good, and bundles everything nicely together.
In the package controller, create a public function named `on_start()´.
Now decide for a URL structure.
I would make a url prefix, let's call it api, just to make it crystal clear that you are accessing the API.
In the on_start() function, you will add the API URLs, like so:
public function on_start() {
Route::register('/api/foo', 'Concrete\Package\*package-name*\*ClassName*::*function-1*');
Route::register('/api/bar', 'Concrete\Package\*package-name*\*ClassName*::*function-2*');
}
The above assumes that you have another class in your package named ClassName with the functions function-1() and function-2().
So whenever you access //domain.abc/api/foo function-1() in ClassName will be called.
If pretty URLs aren't enabled, it will be //domain.abc/index.php/api/foo
But I want to be able to pass parameters as part of the URL
Don't worry! You just add {paramName} somewhere in the path. Like this
Route::register('/api/foo/{paramName}', 'Concrete\Package\*package-name*\*ClassName*::*function-1*');
And then add the same parameter in the function, so it would become function-1($paramName). Remember to keep the names the same!
The parameter(s) can also be in the middle of the url, like /api/{paramName}/foo.
Currently it doesn't seems like there is a way to pass optional parameters directly in Concrete5. So I would suggest that you instead register several versions, with and without the optional parameters.
However, there is an open issue on this on GitHub: Here
As an alternative to multiple URLs for optional parameters, you could get those via GET or POST variables in the request
But I want to do that sexy REST thing with GET, POST, DELETE, etc.
I haven't done this before, so this will just be how I imagine I would do it
For a URL that should act differently for i.e. GET and POST, start of by calling the same function. This function would then check the $_SERVER['REQUEST_METHOD'] and redirect to accurate real function.
For example, let's look at function-2().
function function-2() {
switch ($_SERVER['REQUEST_METHOD']) {
case 'PUT':
$this->function-2_put();
break;
case 'POST':
$this->function-2_post();
break;
case 'GET':
$this->function-2_get();
break;
case 'HEAD':
$this->function-2_head();
break;
case 'DELETE':
$this->function-2_delete();
break;
case 'OPTIONS':
$this->function-2_options();
break;
default:
$this->function-2_error();
break;
}
}
Of course you only need to add the cases that applies to the specific case, and you can default to any function you want.
I hope this gave some insight, that you can work with. Let me know if you need some more specific cases.
I get the following error within Magento CE 1.6.1.0
Warning: session_start() [<a href='function.session-start'>function.session-start</a>]: Cannot send session cookie - headers already sent by (output started at /home/dev/env/var/www/user/dev/wdcastaging/lib/Zend/Controller/Response/Abstract.php:586) in /home/dev/env/var/www/user/dev/wdcastaging/app/code/core/Mage/Core/Model/Session/Abstract/Varien.php on line 119
when accessing /api/soap/?wsdl
Apparently, a session_start() is being attempted after the entire contents of the WSDL file have already been output, resulting in the error.
Why is magento attempting to start a session after outputting all the datums? I'm glad you asked. So it looks like controller_front_send_response_after is being hooked by Mage_Persistent in order to call synchronizePersistentInfo(), which in turn ends up getting that session_start() to fire.
The interesting thing is that this wasn't always happening, initially the WSDL loaded just fine for me, initially I racked my brains to try and see what customization may have been made to our install to cause this, but the tracing I've done seems to indicate that this is all happening entirely inside of core.
We have also experienced a tiny bit of (completely unrelated) strangeness with Mage_Persistent which makes me a little more willing to throw my hands up at this point and SO it.
I've done a bit of searching on SO and have found some questions related to the whole "headers already sent" thing in general, but not this specific case.
Any thoughts?
Oh, and the temporary workaround I have in place is simply disabling Mage_Persistent via the persistent/options/enable config data. I also did a little bit of digging as to whether it might be possible to observe an event in order to disable this module only for the WSDL controller (since that seems to be the only one having problems), but it looks like that module relies exclusively on this config flag to determine it's enabled status.
UPDATE: Bug has been reported: http://www.magentocommerce.com/bug-tracking/issue?issue=13370
I'd report this is a bug to the Magento team. The Magento API controllers all route through standard Magento action controller objects, and all these objects inherit from the Mage_Api_Controller_Action class. This class has a preDispatch method
class Mage_Api_Controller_Action extends Mage_Core_Controller_Front_Action
{
public function preDispatch()
{
$this->getLayout()->setArea('adminhtml');
Mage::app()->setCurrentStore('admin');
$this->setFlag('', self::FLAG_NO_START_SESSION, 1); // Do not start standart session
parent::preDispatch();
return $this;
}
//...
}
which includes setting a flag to ensure normal session handling doesn't start for API methods.
$this->setFlag('', self::FLAG_NO_START_SESSION, 1);
So, it sounds like there's code in synchronizePersistentInf that assumes the existence of a session object, and when it uses it the session is initialized, resulting in the error you've seen. Normally, this isn't a problem as every other controller has initialized a session at this point, but the API controllers explicitly turns it off.
As far as fixes go, your best bet (and probably the quick answer you'll get from Magento support) will be to disable the persistant cart feature for the default configuration setting, but then enable it for specific stores that need it. This will let carts
Coming up with a fix on your own is going to be uncharted territory, and I can't think of a way to do it that isn't terribly hacky/unstable. The most straight forward way would be a class rewrite on the synchronizePersistentInf that calls it's parent method unless you've detected this is an API request.
This answer is not meant to replace the existing answer. But I wanted to drop some code in here in case someone runs into this issue, and comments don't really allow for code formatting.
I went with a simple local code pool override of Mage_Persistent_Model_Observer_Session to exit out of the function for any URL routes that are within /api/*
Not expecting this fix to need to be very long-lived or upgrade-friendly, b/c I'm expecting them to fix this in the next release or so.
public function synchronizePersistentInfo(Varien_Event_Observer $observer)
{
...
if ($request->getRouteName() == 'api') {
return;
}
...
}
So I am fairly new to website development, PHP, Mysql, etc. so it's a given if I get some downvotes for my sheer lack of intelligence, I just want the answer haha.
I have probably jumped the band wagon or probably inherited a completely bad coding practice; instead of simplistic website structures such as stackoverflow.com/questions.php?q=ask (displaying content based on GET data), or making it even more simplistic such as stackoverflow.com/ask.php, etc, we have the seemingly straight forward stackoverflow.com/questions/ask
So what's the weird magic going on?
It's likely you're looking for mod_rewrite.
mod_rewrite will work but it really won't improve your actual file structure on the site (behind the scenes.)
For that you would use a PHP framework. I would suggest starting with CodeIgniter which is simpler than Zend. (I don't have experience with CakePHP so I won't comment on that.)
You will need to configure routing to trap a url so that it maps to a certain controller the use a function to capture the rest of the url as parameters.
function _remap($params = array()) {
return call_user_func_array(array($this, 'index'), $params);
}
Then, in the same controller change the index function like this:
function index($id = null) {
$data['question'] = /* get your data from the database */;
$this->load->view('index', $data);
return true;
}
This assumes you started with the welcome controller example in the zip file.
But, to answer your question more directly, there isn't really any magic going on. The browser is requesting a certain resource and the server is returning that resource according to it's own logic and how it is configured. The layout of files on the server is an internal issue, the browser only sees the representation of the server's state. Read up on the REST principle to understand this better.
Assume a blackboard type application. There are 2 Projects - ProjectA and ProjectB. User 'nupul' (me) is part of both projects. For A I'm an admin and for B I'm just a 'member' (no admin rights)
When accessing the resource at /MySite/ProjectA/Items I want to check if the user is an admin or not.
I know it can be simply done by picking out the {projectName} parameter from the request and using the identifier (of the user making the request) and forwarding that to check against a DB etc.,
My question is 'how' can I add the roles using an Enroler 'during' authentication itself. Since I don't have access to the {projectName} parameter at that stage. I don't know if you have to use Groups/Realms etc., to make this work, but honestly it's just taking me tooooooooooooooooooo long to even understand how to effectively use this? (i.e., before the request is forwarded to the resource)
I mean I know I can create these groups/realms but how do I access the correct 'role' from the resource???? Restlet seriously needs to have more realistic examples and a much better documentation showing the use of it's classes!! It's driving me insane!! Authentication shouldn't be THIS DIFFICULT! :)
The way to do what you want is to split your routers basing on project name within your application (method createInboundRoot). In this case, the projectname will be evaluated before calling the authenticator. See below some examples of implementing such approach:
public Restlet createInboundRoot() {
Router rootRouter = new Router(getContext());
rootRouter.setDefaultMatchingMode(Template.MODE_STARTS_WITH);
rootRouter.attach("/{projectname}/", createApplicationForProject());
return rootRouter;
}
private Restlet createApplicationForProject() {
Router router = new Router(getContext());
ChallengeAuthenticator guard
= new ChallengeAuthenticator(getContext(),
ChallengeScheme.HTTP_BASIC, "realm");
guard.setVerifier(verifier);
guard.setEnroler(enroler);
guard.setNext(router);
router.attach("items", ItemsServerResource.class);
return guard;
}
Using such approach, you'll have access to the value of the projectname variable within the verifier and be able to use it in the authentication processing.
Hope it helps you,
Thierry