initial commit

This commit is contained in:
skidoodle 2024-05-31 12:56:54 +02:00
commit ee9309da68
6 changed files with 436 additions and 0 deletions

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM golang:alpine as builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o iphistory .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/iphistory .
EXPOSE 3000
CMD ["./iphistory"]

15
docker-compose.yaml Normal file
View file

@ -0,0 +1,15 @@
version: '3.9'
services:
iphistory:
image: ghcr.io/skidoodle/iphistory:main
container_name: iphistory
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- data:/app
volumes:
data:
driver: local

27
genhistory.py Normal file
View file

@ -0,0 +1,27 @@
import random
from datetime import datetime, timedelta
import sys
def generate_example_data(num_entries):
data = []
for _ in range(num_entries):
timestamp = datetime.now() - timedelta(days=random.randint(0, 365),
hours=random.randint(0, 23),
minutes=random.randint(0, 59),
seconds=random.randint(0, 59))
ip_address = f"{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}"
entry = f"{timestamp.strftime('%Y-%m-%d %H:%M:%S')} CEST - {ip_address}"
data.append(entry)
return data
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python genhistory.py <num_entries>")
sys.exit(1)
num_entries = int(sys.argv[1])
example_data = generate_example_data(num_entries)
with open('ip_history.txt', 'w') as file:
for entry in example_data:
file.write(entry + '\n')

221
index.html Normal file
View file

@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IP History</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #222;
color: #fff;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
text-align: center;
margin-top: 20px;
margin-bottom: 10px;
}
button.export {
background-color: #007bff;
color: #fff;
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
position: absolute;
top: 20px;
left: 20px;
}
button.export:hover {
background-color: #0056b3;
}
table {
width: 80%;
border-collapse: collapse;
margin: 0 auto;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
background-color: #333;
margin-bottom: 20px;
}
th, td {
border: 1px solid #444;
padding: 12px;
text-align: left;
}
th {
cursor: pointer;
background-color: #007bff;
color: #fff;
}
th.sort-asc::after, th.sort-desc::after {
content: "-ascending";
color: #fff;
}
th.sort-desc::after {
content: "-descending";
}
tbody tr:nth-child(even) {
background-color: #444;
}
.pagination {
margin-top: 20px;
}
.pagination button {
background-color: #007bff;
color: #fff;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 5px;
margin-right: 5px;
}
.pagination button:hover {
background-color: #0056b3;
}
.pagination button.active {
background-color: #0056b3;
}
</style>
</head>
<body>
<button class="export" onclick="exportToCSV()">Export to CSV</button>
<h1>IP History</h1>
<table id="ipTable">
<thead>
<tr>
<th onclick="sortTable()">Timestamp</th>
<th>IP Address</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="pagination" id="pagination">
<button onclick="prevPage()" id="prevBtn">&laquo; Prev</button>
<button onclick="nextPage()" id="nextBtn">Next &raquo;</button>
</div>
<script>
let ipHistory = [];
let currentPage = 1;
const entriesPerPage = 10;
function loadIPHistory() {
fetch('/history')
.then(response => response.json())
.then(data => {
ipHistory = data.map(entry => entry.split(" - "));
displayTable();
})
.catch(error => console.error('Error fetching IP history:', error));
}
function displayTable() {
const tbody = document.getElementById('ipTable').getElementsByTagName('tbody')[0];
tbody.innerHTML = '';
const start = (currentPage - 1) * entriesPerPage;
const end = start + entriesPerPage;
const currentEntries = ipHistory.slice(start, end);
currentEntries.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.textContent = cell;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
updatePaginationButtons();
}
function updatePaginationButtons() {
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
if (currentPage === 1) {
prevBtn.style.display = 'none';
} else {
prevBtn.style.display = 'inline-block';
}
if (currentPage === Math.ceil(ipHistory.length / entriesPerPage)) {
nextBtn.style.display = 'none';
} else {
nextBtn.style.display = 'inline-block';
}
}
function nextPage() {
if (currentPage < Math.ceil(ipHistory.length / entriesPerPage)) {
currentPage++;
displayTable();
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
displayTable();
}
}
function sortTable() {
const table = document.getElementById('ipTable');
const tbody = table.getElementsByTagName('tbody')[0];
const rows = Array.from(tbody.getElementsByTagName('tr'));
const th = table.getElementsByTagName('th')[0];
const isAscending = !th.classList.contains('sort-asc');
const orderModifier = isAscending ? 1 : -1;
rows.sort((a, b) => {
const aText = a.getElementsByTagName('td')[0].textContent;
const bText = b.getElementsByTagName('td')[0].textContent;
return aText.localeCompare(bText) * orderModifier;
});
th.classList.toggle('sort-asc', isAscending);
th.classList.toggle('sort-desc', !isAscending);
rows.forEach(row => tbody.appendChild(row));
}
function exportToCSV() {
const rows = [['Timestamp', 'IP Address'], ...ipHistory];
let csvContent = "data:text/csv;charset=utf-8,";
rows.forEach(rowArray => {
const row = rowArray.join(",");
csvContent += row + "\r\n";
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "ip_history.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
window.onload = loadIPHistory;
</script>
</body>
</html>

0
ip_history.txt Normal file
View file

160
main.go Normal file
View file

@ -0,0 +1,160 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
url = "https://one.one.one.one/cdn-cgi/trace"
filePath = "ip_history.txt"
checkInterval = 30 * time.Second
maxRetries = 3
retryDelay = 10 * time.Second
)
func FetchPublicIP() (string, error) {
var ip string
var err error
for attempt := 1; attempt <= maxRetries; attempt++ {
ip, err = fetchIP()
if err == nil {
return ip, nil
}
fmt.Printf("Attempt %d: Error fetching IP: %v\n", attempt, err)
time.Sleep(retryDelay)
}
return "", err
}
func fetchIP() (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
for _, line := range strings.Split(string(body), "\n") {
if strings.HasPrefix(line, "ip=") {
return strings.TrimPrefix(line, "ip="), nil
}
}
return "", fmt.Errorf("IP address not found")
}
func ReadIPHistory() ([]string, error) {
file, err := os.Open(filePath)
if os.IsNotExist(err) {
return []string{}, nil
} else if err != nil {
return nil, err
}
defer file.Close()
var history []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
history = append(history, scanner.Text())
}
return history, scanner.Err()
}
func WriteIPHistory(history []string) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
writer := bufio.NewWriter(file)
for _, entry := range history {
_, err := writer.WriteString(entry + "\n")
if err != nil {
return err
}
}
return writer.Flush()
}
func TrackIP(ipChan <-chan string) {
var lastLoggedIP string
for ip := range ipChan {
history, err := ReadIPHistory()
if err != nil {
fmt.Println("Error reading IP history:", err)
continue
}
entry := fmt.Sprintf("%s - %s", time.Now().Format("2006-01-02 15:04:05 MST"), ip)
if len(history) == 0 || !strings.Contains(history[0], ip) {
history = append([]string{entry}, history...)
if err := WriteIPHistory(history); err != nil {
fmt.Println("Error writing IP history:", err)
}
fmt.Printf("📄 New IP logged: %s\n", entry)
lastLoggedIP = ip
} else {
if lastLoggedIP != ip {
fmt.Println("🤷 IP is already up to date")
lastLoggedIP = ip
}
}
}
}
func ServeWeb() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html")
})
http.HandleFunc("/history", func(w http.ResponseWriter, r *http.Request) {
history, err := ReadIPHistory()
if err != nil {
http.Error(w, "Error reading IP history", http.StatusInternalServerError)
return
}
jsonData, err := json.Marshal(history)
if err != nil {
http.Error(w, "Error converting history to JSON", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsonData)
})
fmt.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}
func main() {
ipChan := make(chan string)
go func() {
for {
ip, err := FetchPublicIP()
if err != nil {
fmt.Println("Error fetching public IP:", err)
} else {
ipChan <- ip
}
time.Sleep(checkInterval)
}
}()
go TrackIP(ipChan)
ServeWeb()
}