]> git.ipfire.org Git - ipfire-2.x.git/blob - html/cgi-bin/extrahd.cgi
extrahd.cgi: Fix broken UUID check
[ipfire-2.x.git] / html / cgi-bin / extrahd.cgi
1 #!/usr/bin/perl
2 ###############################################################################
3 # #
4 # IPFire.org - A linux based firewall #
5 # Copyright (C) 2023 IPFire Team <info@ipfire.org> #
6 # #
7 # This program is free software: you can redistribute it and/or modify #
8 # it under the terms of the GNU General Public License as published by #
9 # the Free Software Foundation, either version 3 of the License, or #
10 # (at your option) any later version. #
11 # #
12 # This program is distributed in the hope that it will be useful, #
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15 # GNU General Public License for more details. #
16 # #
17 # You should have received a copy of the GNU General Public License #
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
19 # #
20 ###############################################################################
21
22 use strict;
23 # enable only the following on debugging purpose
24 #use warnings;
25 #use CGI::Carp 'fatalsToBrowser';
26
27 require '/var/ipfire/general-functions.pl';
28 require "${General::swroot}/lang.pl";
29 require "${General::swroot}/header.pl";
30
31 my %extrahdsettings = ();
32 my $errormessage = "";
33
34 # Hash to store the configured drives.
35 my %configured_drives;
36
37 # SYSFS directory which contains all block device data.
38 my $sysfs_block_dir = "/sys/class/block";
39
40 # Array which contains the valid mount directories.
41 # Only mounting to subdirectories inside them is allowed.
42 my @valid_mount_dirs = (
43 "/data",
44 "/media",
45 "/mnt",
46 );
47
48 # Grab all available block devices.
49 my @devices = &get_block_devices();
50
51 # Grab all known UUID's.
52 my %uuids = &get_uuids();
53
54 # Grab all mountpoints.
55 my %mountpoints = &get_mountpoints();
56
57 # Omit the file system types of the mounted devices.
58 my %filesystems = &get_mountedfs();
59
60 # Gather all used swap devices.
61 my @swaps = &get_swaps();
62
63 # The config file which contains the configured devices.
64 my $devicefile = "/var/ipfire/extrahd/devices";
65
66 #workaround to suppress a warning when a variable is used only once
67 my @dummy = ( ${Header::colourgreen}, ${Header::colourred} );
68 undef (@dummy);
69
70 &Header::showhttpheaders();
71
72 ### Values that have to be initialized
73 $extrahdsettings{'PATH'} = '';
74 $extrahdsettings{'FS'} = '';
75 $extrahdsettings{'DEVICE'} = '';
76 $extrahdsettings{'ACTION'} = '';
77 $extrahdsettings{'UUID'} = '';
78
79 &Header::getcgihash(\%extrahdsettings);
80
81 &Header::openpage('ExtraHD', 1, '');
82 &Header::openbigbox('100%', 'left', '', $errormessage);
83
84 ############################################################################################################################
85 ############################################################################################################################
86
87 #
88 ## Add a new device.
89 #
90 if ($extrahdsettings{'ACTION'} eq $Lang::tr{'add'}) {
91 # Check if a mount path has been given.
92 if (not $extrahdsettings{'PATH'}) {
93 $errormessage = "$Lang::tr{'extrahd no mount point given'}.";
94
95 # Check if a valid mount path has been choosen.
96 } elsif(not &is_valid_dir("$extrahdsettings{'PATH'}")) {
97 $errormessage = "$Lang::tr{'extrahd you cant mount'} $extrahdsettings{'DEVICE'} $Lang::tr{'extrahd to'} $extrahdsettings{'PATH'} $Lang::tr{'extrahd because it is outside the allowed mount path'}.";
98
99 # Check if the given path allready is mounted somewhere.
100 } elsif(&is_mounted("$extrahdsettings{'PATH'}")) {
101 $errormessage = "$Lang::tr{'extrahd you cant mount'} $extrahdsettings{'DEVICE'} $Lang::tr{'extrahd to'} $extrahdsettings{'PATH'} $Lang::tr{'extrahd because there is already a device mounted'}.";
102 }
103
104 # Check against may previously configured drives.
105 unless ($errormessage) {
106 # Open device file for reading.
107 open( FILE, "< $devicefile" ) or die "Unable to read $devicefile";
108 my @devices = <FILE>;
109 close FILE;
110
111 # Loop through the entries line-by-line.
112 foreach my $entry (sort @devices) {
113 # Split the line into pieces and assign nice variables.
114 my ($uuid, $fs, $path) = split( /\;/, $entry );
115
116 # Remove tailing UUID= from uuid string.
117 $uuid =~ s{^UUID=}{};
118
119 # Check if the path is allready used.
120 if ( "$extrahdsettings{'PATH'}" eq "$path" ) {
121 $errormessage = "$Lang::tr{'extrahd you cant mount'} $extrahdsettings{'DEVICE'} $Lang::tr{'extrahd to'} $extrahdsettings{'PATH'} $Lang::tr{'extrahd because there is already a device mounted'}.";
122 }
123
124 # Check if the uuid is allready used.
125 if ("$extrahdsettings{'UUID'}" eq "$uuid") {
126 $errormessage = "$extrahdsettings{'DEVICE'} is allready mounted.";
127 }
128 }
129 }
130
131 # Go further if there was no error message.
132 unless($errormessage) {
133 # Re-open the device file for writing.
134 open(FILE, ">> $devicefile" ) or die "Unable to write $devicefile";
135
136 # Write the config line.
137 print FILE "UUID=$extrahdsettings{'UUID'};$extrahdsettings{'FS'};$extrahdsettings{'PATH'};\n";
138
139 # Close file handle.
140 close(FILE);
141
142 # Call helper binary to mount the device.
143 &General::system("/usr/local/bin/extrahdctrl", "mount", "$extrahdsettings{'PATH'}");
144 }
145
146 #
147 # Remove an existing one.
148 #
149 } elsif ($extrahdsettings{'ACTION'} eq $Lang::tr{'delete'}) {
150 # Call helper binary to unmount the device.
151 unless(&General::system("/usr/local/bin/extrahdctrl", "umount", "$extrahdsettings{'PATH'}")) {
152 # Open the device file for reading.
153 open(FILE, "< $devicefile" ) or die "Unable to read $devicefile";
154
155 # Read the file content into a temporary array.
156 my @tmp = <FILE>;
157
158 # Close file handle.
159 close(FILE);
160
161 # Re-open device file for writing.
162 open(FILE, "> $devicefile" ) or die "Unable to write $devicefile";
163
164 # Loop through the previous read file content.
165 foreach my $line (sort @tmp) {
166 # Split line content and assign nice variables.
167 my ($uuid, $fs, $path) = split( /\;/, $line );
168
169 # Write the line in case it does not contain our element to delete.
170 if ($path ne $extrahdsettings{'PATH'}) {
171 print FILE "$line";
172 }
173 }
174
175 # Close file handle.
176 close(FILE);
177 } else {
178 $errormessage = "$Lang::tr{'extrahd cant umount'} $extrahdsettings{'PATH'}$Lang::tr{'extrahd maybe the device is in use'}?";
179 }
180 }
181
182 if ($errormessage) {
183 &Header::openbox('100%', 'left', $Lang::tr{'error messages'});
184 print "<class name='base'>$errormessage\n";
185 print "&nbsp;</class>\n";
186 &Header::closebox();
187 }
188
189 ############################################################################################################################
190 ############################################################################################################################
191
192 &Header::openbox('100%', 'center', $Lang::tr{'extrahd detected drives'});
193
194 # Re-read mountpoints.
195 %mountpoints = &get_mountpoints();
196
197 # Read-in the device config file.
198 open( FILE, "< $devicefile" ) or die "Unable to read $devicefile";
199
200 # Loop through the file content.
201 while (<FILE>) {
202 # Cut the line into pieces.
203 my ($uuid, $fs, $path) = split( /\;/, $_ );
204
205 # Add the found entry to the hash of configured drives.
206 $configured_drives{$uuid} = $path;
207 }
208
209 # Close the file handle.
210 close(FILE);
211
212 print <<END
213 <table border='0' width='600' cellspacing="0">
214 END
215 ;
216 foreach my $device (sort @devices) {
217 # Grab the device details.
218 my $vendor = &get_device_vendor($device);
219 my $model = &get_device_model($device);
220 my $bsize = &get_device_size($device);
221
222 # Convert size into human-readable format.
223 my $size = &General::formatBytes($bsize);
224
225 print <<END
226 <tr><td colspan="5">&nbsp;</td></tr>
227 <tr><td align='left' colspan="2"><b>/dev/$device</b></td>
228 <td align='center' colspan="2">$vendor $model</td>
229
230 <td align='center'>$Lang::tr{'size'} $size</td>
231 <td>&nbsp;</td></tr>
232 <tr><td colspan="5">&nbsp;</td></tr>
233 END
234 ;
235
236 # Grab the known partitions of the current block device.
237 my @partitions = &get_device_partitions($device);
238
239 foreach my $partition (@partitions) {
240 my $disabled;
241
242 # Omit the partition size.
243 my $bsize = &get_device_size($partition);
244
245 # Convert into human-readable format.
246 my $size = &General::formatBytes($bsize);
247
248 # Try to omit the used filesystem.
249 my $fs = $filesystems{$partition};
250
251 # Get the mountpoint.
252 my $mountpoint = $mountpoints{$partition};
253
254 # If no mountpoint could be determined try to grab from
255 # configured drives.
256 unless($mountpoint) {
257 my $uuid = $uuids{$partition};
258
259 # Build uuid string.
260 $uuid = "UUID=" . $uuid;
261
262 # Try to obtain a possible moutpoint from configured drives.
263 $mountpoint = $configured_drives{$uuid} if ($configured_drives{$uuid});
264 }
265
266 # Check if the mountpoint is used as root or boot device.
267 if ($mountpoint eq "/" or $mountpoint =~ "^/boot") {
268 $disabled = "disabled";
269
270 # Check if it is mounted.
271 } elsif(&is_mounted($mountpoint)) {
272 $disabled = "disabled";
273
274 # Check if the device is used as swap.
275 } elsif (&is_swap($partition)) {
276 $disabled = "disabled";
277 $mountpoint = "swap";
278 $fs = "swap";
279 }
280
281 print <<END
282
283 <form method='post' action='$ENV{'SCRIPT_NAME'}'>
284 <tr><td align="left" colspan=5><strong>UUID=$uuids{$partition}</strong></td></tr>
285 <tr>
286 <td align="list">/dev/$partition</td>
287 <td align="center">$Lang::tr{'size'} $size</td>
288 <td align="center">$fs</td>
289 <td align="center"><input type='text' name='PATH' value='$mountpoint' $disabled></td>
290 <td align="center">
291 <input type='hidden' name='DEVICE' value='/dev/$partition' />
292 <input type='hidden' name='UUID' value='$uuids{$partition}' />
293 END
294 ;
295 # Check if the mountpoint refers to a known configured drive.
296 if(&is_configured($mountpoint)) {
297 print "<input type='hidden' name='ACTION' value='$Lang::tr{'delete'}'>\n";
298 print "<input type='hidden' name='PATH' value='$mountpoint'>\n";
299
300 # Check if the device is mounted properly.
301 if(&is_mounted($mountpoint)) {
302 print "<img src='/images/updbooster/updxl-led-green.gif' alt='$Lang::tr{'extrahd mounted'}' title='$Lang::tr{'extrahd mounted'}'>&nbsp;\n";
303 } else {
304 print "<img src='/images/updbooster/updxl-led-red.gif' alt='$Lang::tr{'extrahd not mounted'}' title='$Lang::tr{'extrahd not mounted'}'>&nbsp;\n";
305 }
306
307 print "<input type='image' alt='$Lang::tr{'delete'}' title='$Lang::tr{'delete'}' src='/images/delete.gif'>\n";
308 } else {
309 unless($disabled) {
310 print "<input type='hidden' name='ACTION' value='$Lang::tr{'add'}'>\n";
311 print "<input type='hidden' name='FS' value='auto'>\n";
312 print "<img src='/images/updbooster/updxl-led-gray.gif' alt='$Lang::tr{'extrahd not configured'}' title='$Lang::tr{'extrahd not configured'}'>&nbsp;\n";
313 print "<input type='image' alt='$Lang::tr{'add'}' title='$Lang::tr{'add'}' src='/images/add.gif'>\n";
314 }
315 }
316
317 print <<END
318 </form></td></tr>
319 END
320 ; }
321
322 }
323
324 print <<END
325 <tr><td align="center" colspan="5">&nbsp;</td></tr>
326 <tr><td align="center" colspan="5">&nbsp;</td></tr>
327 <tr><td align="center" colspan="5">$Lang::tr{'extrahd install or load driver'}</td></tr>
328 </table>
329 END
330 ;
331 &Header::closebox();
332
333 &Header::closebigbox();
334 &Header::closepage();
335
336 #
337 ## Function which return an array with all available block devices.
338 #
339 sub get_block_devices () {
340 my @devices;
341
342 # Open directory from kernel sysfs.
343 opendir(DEVICES, "/sys/block");
344
345 # Loop through the directory.
346 while(readdir(DEVICES)) {
347 # Skip . and ..
348 next if($_ =~ /^\.$/);
349 next if($_ =~ /^\..$/);
350
351 # Skip any loopback and ram devices.
352 next if($_ =~ "^loop");
353 next if($_ =~ "^ram");
354
355 # Add the device to the array of found devices.
356 push(@devices, $_);
357 }
358
359 # Close directory handle.
360 closedir(DEVICES);
361
362 # Return the devices array.
363 return @devices;
364 }
365
366 #
367 ## Function which return all partitions of a given block device.
368 #
369 sub get_device_partitions ($) {
370 my ($device) = @_;
371
372 # Array to store the known partitions for the given
373 # device.
374 my @partitions;
375
376 # Assign device directory.
377 my $device_dir = "$sysfs_block_dir/$device";
378
379 # Abort and return nothing if the device dir does not exist.
380 return unless(-d "$device_dir");
381
382 opendir(DEVICE, "$sysfs_block_dir/$device");
383 while(readdir(DEVICE)) {
384 next unless($_ =~ "^$device");
385
386 push(@partitions, $_);
387 }
388
389 closedir(DEVICE);
390
391 @partitions = sort(@partitions);
392
393 return @partitions;
394 }
395
396 #
397 ## Returns the vendor of a given block device.
398 #
399 sub get_device_vendor ($) {
400 my ($device) = @_;
401
402 # Assign device directory.
403 my $device_dir = "$sysfs_block_dir/$device";
404
405 # Abort and return nothing if the device dir does not exist
406 # or no vendor file exists.
407 return unless(-d "$device_dir");
408 return unless(-f "$device_dir/device/vendor");
409
410 # Open and read-in the device vendor.
411 open(VENDOR, "$device_dir/device/vendor");
412 my $vendor = <VENDOR>;
413 close(VENDOR);
414
415 # Abort and return nothing if no vendor could be read.
416 return unless($vendor);
417
418 # Remove any newlines from the vendor string.
419 chomp($vendor);
420
421 # Return the omited vendor.
422 return $vendor;
423 }
424
425 #
426 ## Returns the model name (string) of a given block device.
427 #
428 sub get_device_model ($) {
429 my ($device) = @_;
430
431 # Assign device directory.
432 my $device_dir = "$sysfs_block_dir/$device";
433
434 # Abort and return nothing if the device dir does not exist
435 # or no model file exists.
436 return unless(-d "$device_dir");
437 return unless(-f "$device_dir/device/model");
438
439 # Open and read-in the device model.
440 open(MODEL, "$device_dir/device/model");
441 my $model = <MODEL>;
442 close(MODEL);
443
444 # Abort and return nothing if no model could be read.
445 return unless($model);
446
447 # Remove any newlines from the model string.
448 chomp($model);
449
450 # Return the model string.
451 return $model;
452 }
453
454 #
455 ## Returns the size of a given device in bytes.
456 #
457 sub get_device_size ($) {
458 my ($device) = @_;
459
460 # Assign device directory.
461 my $device_dir = "$sysfs_block_dir/$device";
462
463 # Abort and return nothing if the device dir does not exist
464 # or no size file exists.
465 return unless(-d "$device_dir");
466 return unless(-f "$device_dir/size");
467
468 # Open and read-in the device size.
469 open(SIZE, "$device_dir/size");
470 my $size = <SIZE>;
471 close(SIZE);
472
473 # Abort and return nothing if the size could not be read.
474 return unless($size);
475
476 # Remove any newlines for the size string.
477 chomp($size);
478
479 # The omited size only contains the amount of blocks from the
480 # given device. To convert this into bytes we have to multiply this
481 # value with 512 bytes for each block. This is a static value used by
482 # the linux kernel.
483 $size = $size * 512;
484
485 # Return the size in bytes.
486 return $size;
487 }
488
489 #
490 ## Function which returns all currently mounted devices as a hash.
491 ## example: "sda1" -> "/boot"
492 #
493 sub get_mountpoints () {
494 my %mounts;
495
496 # Open and read-in the current mounts from the
497 # kernel file system.
498 open(MOUNT, "/proc/mounts");
499
500 # Loop through the known mounts.
501 while(<MOUNT>) {
502 # Skip mounts which does not belong to a device.
503 next unless ($_ =~ "^/dev");
504
505 # Cut the line into pieces and assign nice variables.
506 my ($dev, $mpoint, $fs, $options, $a, $b) = split(/ /, $_);
507
508 # Split the device name.
509 my @tmp = split("/", $dev);
510
511 # Assign the plain device name to a new variable.
512 # It is the last element of the array.
513 my $device = $tmp[-1];
514
515 # Add the mountpoint to the hash of mountpoints.
516 $mounts{"$device"} = $mpoint;
517 }
518
519 # Close file handle.
520 close(MOUNT);
521
522 # Return the hash of known mountpoints.
523 return %mounts;
524 }
525
526 sub get_swaps () {
527 my @swaps;
528
529 # Open and read the swaps file.
530 open(SWAP, "/proc/swaps");
531
532 # Loop though the file content.
533 while(<SWAP>) {
534 # Skip lines which does not belong to a device.
535 next unless ($_ =~ "^/dev");
536
537 # Split the line and assign nice variables.
538 my ($dev, $type, $size, $used, $prio) = split(/ /, $_);
539
540 # Cut the device line into pieces.
541 my @tmp = split("/", $dev);
542
543 my $device = @tmp[-1];
544
545 # Add the found swap to the array of swaps.
546 push(@swaps, $device);
547 }
548
549 # Close file handle.
550 close(SWAP);
551
552 # Sort the array.
553 @swaps = sort(@swaps);
554
555 # Return the array.
556 return @swaps;
557 }
558
559 #
560 ## Function with returns the mounted devices and the used filesystems as a hash.
561 ## Example: "sda1" -> "ext4"
562 #
563 sub get_mountedfs () {
564 my %mountedfs;
565
566 # Open and read the current mounts from the kernel
567 # file system.
568 open(MOUNT, "/proc/mounts");
569
570 # Loop through the known mounts.
571 while(<MOUNT>) {
572 # Skip mounts which does not belong to a device.
573 next unless ($_ =~ "^/dev");
574
575 # Split line and assign nice variables.
576 my ($dev, $mpoint, $fs, $options, $a, $b) = split(/ /, $_);
577
578 # Cut the device line into pieces.
579 my @tmp = split("/", $dev);
580
581 # Assign the plain device name to a variable
582 # It is the last element of the temporary array.
583 my $device = $tmp[-1];
584
585 # Convert the filesystem into lower case format.
586 $fs = lc($fs);
587
588 # Add the mounted file system.
589 $mountedfs{$device} = $fs;
590 }
591
592 # Close file handle.
593 close(MOUNT);
594
595 # Return the hash with the mounted filesystems.
596 return %mountedfs;
597 }
598
599 #
600 ## Function which returns all known UUID's as a hash.
601 ## Example: "sda1" -> "1234-5678-abcd"
602 #
603 sub get_uuids () {
604 my %uuids;
605
606 # Directory where the uuid mappings can be found.
607 my $uuid_dir = "/dev/disk/by-uuid";
608
609 # Open uuid directory and read-in the current known uuids.
610 opendir(UUIDS, "$uuid_dir");
611
612 # Loop through the uuids.
613 foreach my $uuid (readdir(UUIDS)) {
614 # Skip . and ..
615 next if($uuid eq "." or $uuid eq "..");
616
617 # Skip everything which is not a symbolic link.
618 next unless(-l "$uuid_dir/$uuid");
619
620 # Resolve the target of the symbolic link.
621 my $target = readlink("$uuid_dir/$uuid");
622
623 # Split the link target into pieces.
624 my @tmp = split("/", $target);
625
626 # Assign the last element of the array to the dev variable.
627 my $dev = "$tmp[-1]";
628
629 # Add the device and uuid to the hash of uuids.
630 $uuids{$dev} = $uuid;
631 }
632
633 # Close directory handle.
634 closedir(UUIDS);
635
636 # Return the hash of uuids.
637 return %uuids;
638 }
639
640 #
641 ## Returns the device name of a given uuid.
642 #
643 sub device_by_uuid ($) {
644 my ($uuid) = @_;
645
646 # Reverse the main uuids hash.
647 my %uuids = reverse %uuids;
648
649 # Lookup and return the device name.
650 return $uuids{$uuid};
651 }
652
653 #
654 ## Returns "True" in case a given path is a known mountpoint.
655 #
656 sub is_mounted ($) {
657 my ($mpoint) = @_;
658
659 my %mountpoints = reverse %mountpoints;
660
661 # Return "True" if the requested mountpoint is known and
662 # therefore mounted.
663 return 1 if($mountpoints{$mpoint});
664 }
665
666 #
667 ## Returns "True" if a given mountpoint is a subdirectory of one
668 ## of the directories specified by the valid_mount_dirs array abouve.
669 #
670 sub is_valid_dir ($) {
671 my ($mpoint) = @_;
672
673 # Do not allow "/mnt" or "/media" as mount points.
674 return if($mpoint eq "/mnt");
675 return if($mpoint eq "/media");
676
677 # Split the given mountpoint into pieces and store them
678 # in a temporay array.
679 my @tmp = split("/", $mpoint);
680
681 # Exit and return nothing if the temporary array is empty.
682 return unless(@tmp);
683
684 # Build the root path based on the given mount point.
685 my $root_path = "/" . @tmp[1];
686
687 # Check if the root path is valid.
688 return 1 if(grep /$root_path/, @valid_mount_dirs);
689 }
690
691 #
692 # Returns "True" if a device is used as swap.
693 #
694 sub is_swap ($) {
695 my ($device) = @_;
696
697 return 1 if(grep /$device/, @swaps);
698 }
699
700 #
701 ## Returns "True" if a drive is a configured one.
702 #
703 sub is_configured ($) {
704 my ($path) = @_;
705
706 # Loop through the hash of configured drives.
707 foreach my $uuid (keys %configured_drives) {
708 return 1 if($configured_drives{$uuid} eq "$path");
709 }
710 }