Read and write to /etc/hosts
TL;DR: - Rename website-blocker to url-blocker - Add a README.md - Reads and writes to /etc/hosts
This commit is contained in:
		
							parent
							
								
									75595b0126
								
							
						
					
					
						commit
						946764f6bd
					
				
					 8 changed files with 182 additions and 25 deletions
				
			
		|  | @ -43,9 +43,9 @@ newtype Hour = Hour { getHour :: Int } deriving (Show, Eq, Generic) | ||||||
| 
 | 
 | ||||||
| newtype Minute = Minute { getMinute :: Int } deriving (Show, Eq, Generic) | newtype Minute = Minute { getMinute :: Int } deriving (Show, Eq, Generic) | ||||||
| 
 | 
 | ||||||
| data EtcHostEntry = EtcHostEntry { ip :: IPAddress | data EtcHostsEntry = EtcHostsEntry { ip :: IPAddress | ||||||
|                                  , domains :: [Domain] |                                    , domains :: [Domain] | ||||||
|                                  } deriving (Show) |                                    } deriving (Show) | ||||||
| 
 | 
 | ||||||
| -- | Write these in terms of your system's local time (i.e. `date`). | -- | Write these in terms of your system's local time (i.e. `date`). | ||||||
| data TimeSlot = TimeSlot { beg :: (Hour, Minute) | data TimeSlot = TimeSlot { beg :: (Hour, Minute) | ||||||
|  | @ -103,6 +103,12 @@ instance Aeson.FromJSON Rule where | ||||||
| -- Functions | -- Functions | ||||||
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
|  | -- | Pipe operator | ||||||
|  | (|>) :: a -> (a -> b) -> b | ||||||
|  | (|>) a f = f a | ||||||
|  | infixl 1 |> | ||||||
|  | 
 | ||||||
|  | -- | Returns True if the current time falls within any of the `timeslots`. | ||||||
| isWithinTimeSlot :: LocalTime.LocalTime -> [TimeSlot] -> Bool | isWithinTimeSlot :: LocalTime.LocalTime -> [TimeSlot] -> Bool | ||||||
| isWithinTimeSlot date timeslots = | isWithinTimeSlot date timeslots = | ||||||
|   List.any withinTimeSlot timeslots |   List.any withinTimeSlot timeslots | ||||||
|  | @ -115,51 +121,90 @@ isWithinTimeSlot date timeslots = | ||||||
|             LocalTime.localTimeOfDay date |             LocalTime.localTimeOfDay date | ||||||
|       in (todHour > ah) && (todMin > am) && (todHour < bh) && (todMin < bm) |       in (todHour > ah) && (todMin > am) && (todHour < bh) && (todMin < bm) | ||||||
| 
 | 
 | ||||||
|  | -- | Returns True if `day` is the same day as today. | ||||||
| isToday :: LocalTime.LocalTime -> Calendar.DayOfWeek -> Bool | isToday :: LocalTime.LocalTime -> Calendar.DayOfWeek -> Bool | ||||||
| isToday date day = Calendar.dayOfWeek (LocalTime.localDay date) == day | isToday date day = today == day | ||||||
|  |   where | ||||||
|  |     today = Calendar.dayOfWeek (LocalTime.localDay date) | ||||||
| 
 | 
 | ||||||
| isAllowed :: LocalTime.LocalTime -> [Allowance] -> Bool | -- | Returns True if a list of none of the `allowances` are valid. | ||||||
| isAllowed _ [] = False | shouldBeBlocked :: LocalTime.LocalTime -> [Allowance] -> Bool | ||||||
| isAllowed date allowances = do | shouldBeBlocked _ [] = True | ||||||
|  | shouldBeBlocked date allowances = do | ||||||
|   case filter (isToday date . day) allowances of |   case filter (isToday date . day) allowances of | ||||||
|     [Allowance{timeslots}] -> |     [Allowance{timeslots}] -> not $ isWithinTimeSlot date timeslots | ||||||
|       isWithinTimeSlot date timeslots |     [] -> True | ||||||
|     [] -> False |  | ||||||
|     -- Error when more than one rule per day |     -- Error when more than one rule per day | ||||||
|     _  -> False |     _  -> True | ||||||
| 
 | 
 | ||||||
| serializeEntry :: EtcHostEntry -> Text | -- | Maps an EtcHostsEntry to the line of text url-blocker will append to /etc/hosts. | ||||||
| serializeEntry EtcHostEntry{ip, domains} = | serializeEtcHostEntry :: EtcHostsEntry -> Text | ||||||
|  | serializeEtcHostEntry EtcHostsEntry{ip, domains} = | ||||||
|   (getIPAddress ip) <> "\t" <> (Text.unwords $ fmap getDomain domains) |   (getIPAddress ip) <> "\t" <> (Text.unwords $ fmap getDomain domains) | ||||||
| 
 | 
 | ||||||
| toEtcHostEntry :: LocalTime.LocalTime -> Rule -> Maybe EtcHostEntry | -- | Create an EtcHostsEntry mapping the URLs in `rule` to 127.0.0.1 if the | ||||||
| toEtcHostEntry date Rule{urls, allowed} = | -- URLs should be blocked. | ||||||
|   if isAllowed date allowed then | maybeBlockURL :: LocalTime.LocalTime -> Rule -> Maybe EtcHostsEntry | ||||||
|     Nothing | maybeBlockURL date Rule{urls, allowed} = | ||||||
|   else |   if shouldBeBlocked date allowed then | ||||||
|     Just $ EtcHostEntry { ip = IPAddress "127.0.0.1" |     Just $ EtcHostsEntry { ip = IPAddress "127.0.0.1" | ||||||
|                         , domains = fmap (Domain . getURL) urls |                         , domains = fmap (Domain . getURL) urls | ||||||
|                         } |                         } | ||||||
|  |   else | ||||||
|  |     Nothing | ||||||
| 
 | 
 | ||||||
|  | -- | Read and parse the rules.json file. | ||||||
|  | -- TODO(wpcarro): Properly handle errors for file not found. | ||||||
|  | -- TODO(wpcarro): Properly handle errors for parse failures. | ||||||
|  | -- TODO(wpcarro): How can we resolve the $HOME directory when this is run as | ||||||
|  | -- root? | ||||||
| getRules :: IO [Rule] | getRules :: IO [Rule] | ||||||
| getRules = do | getRules = do | ||||||
|   contents <- LazyByteString.readFile "rules.json" |   contents <- LazyByteString.readFile "/home/wpcarro/.config/url-blocker/rules.json" | ||||||
|   let payload = Aeson.eitherDecode contents |   let payload = Aeson.eitherDecode contents | ||||||
|   pure $ Either.fromRight [] payload |   pure $ Either.fromRight [] payload | ||||||
| 
 | 
 | ||||||
| header :: Text | -- | Informational header added to /etc/hosts before the entries that | ||||||
| header = | -- url-blocker adds. | ||||||
|  | urlBlockerHeader :: Text | ||||||
|  | urlBlockerHeader = | ||||||
|   Text.unlines [ "################################################################################" |   Text.unlines [ "################################################################################" | ||||||
|                , "# Added by url-blocker" |                , "# Added by url-blocker." | ||||||
|  |                , "#" | ||||||
|  |                , "# Warning: url-blocker will remove anything that you add beneath this header." | ||||||
|                , "################################################################################" |                , "################################################################################" | ||||||
|                ] |                ] | ||||||
| 
 | 
 | ||||||
|  | -- | Removes all entries that url-blocker may have added to /etc/hosts. | ||||||
|  | removeURLBlockerEntries :: Text -> Text | ||||||
|  | removeURLBlockerEntries etcHosts = | ||||||
|  |   case Text.breakOn urlBlockerHeader etcHosts of | ||||||
|  |     (etcHosts', _) -> etcHosts' | ||||||
|  | 
 | ||||||
|  | -- | Appends the newly created `entries` to `etcHosts`. | ||||||
|  | addURLBlockerEntries :: Text -> Text -> Text | ||||||
|  | addURLBlockerEntries entries etcHosts = | ||||||
|  |   Text.unlines [ etcHosts | ||||||
|  |                , urlBlockerHeader | ||||||
|  |                , entries | ||||||
|  |                ] | ||||||
|  | 
 | ||||||
|  | -- | This script reads the current /etc/hosts, removes any entries that | ||||||
|  | -- url-blocker may have added in a previous run, and adds new entries to block | ||||||
|  | -- URLs according to the rules.json file. | ||||||
| main :: IO () | main :: IO () | ||||||
| main = do | main = do | ||||||
|   rules <- getRules |   rules <- getRules | ||||||
|   tz <- LocalTime.getCurrentTimeZone |   tz <- LocalTime.getCurrentTimeZone | ||||||
|   ct <- Clock.getCurrentTime |   ct <- Clock.getCurrentTime | ||||||
|   let date = LocalTime.utcToLocalTime tz ct |   let date = LocalTime.utcToLocalTime tz ct | ||||||
|       etcHosts = Text.unlines . fmap serializeEntry . Maybe.catMaybes $ fmap (toEtcHostEntry date) rules |       entries = rules | ||||||
|  |                 |> fmap (maybeBlockURL date) | ||||||
|  |                 |> Maybe.catMaybes | ||||||
|  |                 |> fmap serializeEtcHostEntry | ||||||
|  |                 |> Text.unlines | ||||||
|   existingEtcHosts <- TextIO.readFile "/etc/hosts" |   existingEtcHosts <- TextIO.readFile "/etc/hosts" | ||||||
|   TextIO.putStrLn $ existingEtcHosts <> "\n" <> header <> "\n" <> etcHosts |   existingEtcHosts | ||||||
|  |     |> removeURLBlockerEntries | ||||||
|  |     |> addURLBlockerEntries entries | ||||||
|  |     |> \x -> writeFile "/etc/hosts" (Text.unpack x) | ||||||
							
								
								
									
										47
									
								
								tools/url-blocker/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tools/url-blocker/README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | # url-blocker | ||||||
|  | 
 | ||||||
|  | `url-blocker` blocks the URLs that you want to block when you want it to block | ||||||
|  | them. | ||||||
|  | 
 | ||||||
|  | Let's say that you don't want to visit Twitter during the work week. Create the | ||||||
|  | file `~/.config/url-blocker/rules.json` with the following contents and | ||||||
|  | `url-blocker` will take care of the rest. | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | # ~/.config/url-blocker/rules.json | ||||||
|  | [ | ||||||
|  |   { | ||||||
|  |     "urls": [ | ||||||
|  |       "twitter.com", | ||||||
|  |       "www.twitter.com", | ||||||
|  |     ], | ||||||
|  |     "allowed": [ | ||||||
|  |       { | ||||||
|  |         "day": "Saturday", | ||||||
|  |         "timeslots": [ | ||||||
|  |           "00:00-11:59" | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "day": "Sunday", | ||||||
|  |         "timeslots": [ | ||||||
|  |           "00:00-11:59" | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Installation | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | $ nix-env -iA 'briefcase.tools.url-blocker' | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## How does it work? | ||||||
|  | 
 | ||||||
|  | `systemd` is intended to run `url-blocker` once every minute. `url-blocker` will | ||||||
|  | read `/etc/hosts` and map the URLs defined in `rules.json` to `127.0.0.1` when | ||||||
|  | you want them blocked. Because `systemd` run once every minute, `/etc/hosts` | ||||||
|  | should be current to the minute as well. | ||||||
							
								
								
									
										37
									
								
								tools/url-blocker/default.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								tools/url-blocker/default.nix
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | { ... }: | ||||||
|  | 
 | ||||||
|  | let | ||||||
|  |   pkgs = import <unstable> {}; | ||||||
|  | 
 | ||||||
|  |   ghc = pkgs.haskellPackages.ghcWithPackages (hpkgs: [ | ||||||
|  |     hpkgs.time | ||||||
|  |     hpkgs.aeson | ||||||
|  |     hpkgs.either | ||||||
|  |   ]); | ||||||
|  | 
 | ||||||
|  |   # This is the systemd service unit | ||||||
|  |   service = pkgs.stdenv.mkDerivation { | ||||||
|  |     name = "url-blocker"; | ||||||
|  |     src = ./.; | ||||||
|  |     buildInputs = with pkgs; [ | ||||||
|  |     ]; | ||||||
|  |     buildPhase = '' | ||||||
|  |     ${ghc}/bin/ghc Main.hs | ||||||
|  |   ''; | ||||||
|  |     installPhase = '' | ||||||
|  |     mv ./Main $out | ||||||
|  |   ''; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   # This is the systemd timer unit. | ||||||
|  |   # Run once every minute. | ||||||
|  |   # Give root privilege. | ||||||
|  |   systemdUnit = { | ||||||
|  |     systemd = { | ||||||
|  |       timers.simple-timer = { | ||||||
|  |         wantedBy = [ "timers.target" ]; | ||||||
|  |         partOf = []; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  | in null | ||||||
							
								
								
									
										28
									
								
								tools/url-blocker/rules.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								tools/url-blocker/rules.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | [ | ||||||
|  |   { | ||||||
|  |     "urls": [ | ||||||
|  |       "facebook.com", | ||||||
|  |       "www.facebook.com", | ||||||
|  |       "twitter.com", | ||||||
|  |       "www.twitter.com", | ||||||
|  |       "youtube.com", | ||||||
|  |       "www.youtube.com", | ||||||
|  |       "instagram.com", | ||||||
|  |       "www.instagram.com" | ||||||
|  |     ], | ||||||
|  |     "allowed": [] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "urls": [ | ||||||
|  |       "chat.googleplex.com" | ||||||
|  |     ], | ||||||
|  |     "allowed": [ | ||||||
|  |       { | ||||||
|  |         "day": "Sunday", | ||||||
|  |         "timeslots": [ | ||||||
|  |           "18:35-18:39" | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | ] | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue