mirror of
https://github.com/safing/portmaster
synced 2025-09-02 18:49:14 +00:00
Merge pull request #285 from safing/fix/patchset-2
Fix early connection handling and mimetypes
This commit is contained in:
commit
f44fa91d5e
6 changed files with 286 additions and 157 deletions
|
@ -1,46 +0,0 @@
|
||||||
package firewall
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/netenv"
|
|
||||||
"github.com/safing/portmaster/resolver"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
resolver.SetLocalAddrFactory(PermittedAddr)
|
|
||||||
netenv.SetLocalAddrFactory(PermittedAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PermittedAddr returns an already permitted local address for the given network for reliable connectivity.
|
|
||||||
// Returns nil in case of error.
|
|
||||||
func PermittedAddr(network string) net.Addr {
|
|
||||||
switch network {
|
|
||||||
case "udp":
|
|
||||||
return PermittedUDPAddr()
|
|
||||||
case "tcp":
|
|
||||||
return PermittedTCPAddr()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PermittedUDPAddr returns an already permitted local udp address for reliable connectivity.
|
|
||||||
// Returns nil in case of error.
|
|
||||||
func PermittedUDPAddr() *net.UDPAddr {
|
|
||||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", GetPermittedPort()))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// PermittedTCPAddr returns an already permitted local tcp address for reliable connectivity.
|
|
||||||
// Returns nil in case of error.
|
|
||||||
func PermittedTCPAddr() *net.TCPAddr {
|
|
||||||
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", GetPermittedPort()))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
|
|
@ -3,12 +3,14 @@ package firewall
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portmaster/netenv"
|
"github.com/safing/portmaster/netenv"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
"github.com/tevino/abool"
|
||||||
|
|
||||||
|
@ -63,7 +65,6 @@ func interceptionStart() error {
|
||||||
|
|
||||||
interceptionModule.StartWorker("stat logger", statLogger)
|
interceptionModule.StartWorker("stat logger", statLogger)
|
||||||
interceptionModule.StartWorker("packet handler", packetHandler)
|
interceptionModule.StartWorker("packet handler", packetHandler)
|
||||||
interceptionModule.StartWorker("ports state cleaner", portsInUseCleaner)
|
|
||||||
|
|
||||||
return interception.Start()
|
return interception.Start()
|
||||||
}
|
}
|
||||||
|
@ -103,20 +104,60 @@ func handlePacket(ctx context.Context, pkt packet.Packet) {
|
||||||
}
|
}
|
||||||
pkt.SetCtx(traceCtx)
|
pkt.SetCtx(traceCtx)
|
||||||
|
|
||||||
// associate packet to link and handle
|
// Get connection of packet.
|
||||||
conn, ok := network.GetConnection(pkt.GetConnectionID())
|
conn, err := getConnection(pkt)
|
||||||
if ok {
|
if err != nil {
|
||||||
tracer.Tracef("filter: assigned to connection %s", conn.ID)
|
tracer.Errorf("filter: packet %s dropped: %s", pkt, err)
|
||||||
} else {
|
_ = pkt.Drop()
|
||||||
conn = network.NewConnectionFromFirstPacket(pkt)
|
return
|
||||||
tracer.Tracef("filter: created new connection %s", conn.ID)
|
|
||||||
conn.SetFirewallHandler(initialHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle packet
|
// handle packet
|
||||||
conn.HandlePacket(pkt)
|
conn.HandlePacket(pkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var getConnectionSingleInflight singleflight.Group
|
||||||
|
|
||||||
|
func getConnection(pkt packet.Packet) (*network.Connection, error) {
|
||||||
|
created := false
|
||||||
|
|
||||||
|
// Create or get connection in single inflight lock in order to prevent duplicates.
|
||||||
|
newConn, err, shared := getConnectionSingleInflight.Do(pkt.GetConnectionID(), func() (interface{}, error) {
|
||||||
|
// First, check for an existing connection.
|
||||||
|
conn, ok := network.GetConnection(pkt.GetConnectionID())
|
||||||
|
if ok {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else create new one from the packet.
|
||||||
|
conn = network.NewConnectionFromFirstPacket(pkt)
|
||||||
|
conn.SetFirewallHandler(initialHandler)
|
||||||
|
created = true
|
||||||
|
return conn, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %s", err)
|
||||||
|
}
|
||||||
|
if newConn == nil {
|
||||||
|
return nil, errors.New("connection getter returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform and log result.
|
||||||
|
conn := newConn.(*network.Connection)
|
||||||
|
switch {
|
||||||
|
case created && shared:
|
||||||
|
log.Tracer(pkt.Ctx()).Tracef("filter: created new connection %s (shared)", conn.ID)
|
||||||
|
case created:
|
||||||
|
log.Tracer(pkt.Ctx()).Tracef("filter: created new connection %s", conn.ID)
|
||||||
|
case shared:
|
||||||
|
log.Tracer(pkt.Ctx()).Tracef("filter: assigned connection %s (shared)", conn.ID)
|
||||||
|
default:
|
||||||
|
log.Tracer(pkt.Ctx()).Tracef("filter: assigned connection %s", conn.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
// fastTrackedPermit quickly permits certain network criticial or internal connections.
|
// fastTrackedPermit quickly permits certain network criticial or internal connections.
|
||||||
func fastTrackedPermit(pkt packet.Packet) (handled bool) {
|
func fastTrackedPermit(pkt packet.Packet) (handled bool) {
|
||||||
meta := pkt.Info()
|
meta := pkt.Info()
|
||||||
|
@ -265,13 +306,12 @@ func fastTrackedPermit(pkt packet.Packet) (handled bool) {
|
||||||
func initialHandler(conn *network.Connection, pkt packet.Packet) {
|
func initialHandler(conn *network.Connection, pkt packet.Packet) {
|
||||||
log.Tracer(pkt.Ctx()).Trace("filter: handing over to connection-based handler")
|
log.Tracer(pkt.Ctx()).Trace("filter: handing over to connection-based handler")
|
||||||
|
|
||||||
// check for internal firewall bypass
|
// Check for pre-authenticated port.
|
||||||
ps := getPortStatusAndMarkUsed(pkt.Info().LocalPort())
|
if localPortIsPreAuthenticated(conn.Entity.Protocol, conn.LocalPort) {
|
||||||
if ps.isMe {
|
// Approve connection.
|
||||||
// approve
|
|
||||||
conn.Accept("connection by Portmaster", noReasonOptionKey)
|
conn.Accept("connection by Portmaster", noReasonOptionKey)
|
||||||
conn.Internal = true
|
conn.Internal = true
|
||||||
// finish
|
// Finalize connection.
|
||||||
conn.StopFirewallHandler()
|
conn.StopFirewallHandler()
|
||||||
issueVerdict(conn, pkt, 0, true)
|
issueVerdict(conn, pkt, 0, true)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
package firewall
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
"github.com/safing/portbase/rng"
|
|
||||||
)
|
|
||||||
|
|
||||||
type portStatus struct {
|
|
||||||
lastSeen time.Time
|
|
||||||
isMe bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
portsInUse = make(map[uint16]*portStatus)
|
|
||||||
portsInUseLock sync.Mutex
|
|
||||||
|
|
||||||
cleanerTickDuration = 10 * time.Second
|
|
||||||
cleanTimeout = 10 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
func getPortStatusAndMarkUsed(port uint16) *portStatus {
|
|
||||||
portsInUseLock.Lock()
|
|
||||||
defer portsInUseLock.Unlock()
|
|
||||||
|
|
||||||
ps, ok := portsInUse[port]
|
|
||||||
if ok {
|
|
||||||
ps.lastSeen = time.Now()
|
|
||||||
return ps
|
|
||||||
}
|
|
||||||
|
|
||||||
new := &portStatus{
|
|
||||||
lastSeen: time.Now(),
|
|
||||||
isMe: false,
|
|
||||||
}
|
|
||||||
portsInUse[port] = new
|
|
||||||
return new
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPermittedPort returns a local port number that is already permitted for communication.
|
|
||||||
// This bypasses the process attribution step to guarantee connectivity.
|
|
||||||
// Communication on the returned port is attributed to the Portmaster.
|
|
||||||
func GetPermittedPort() uint16 {
|
|
||||||
portsInUseLock.Lock()
|
|
||||||
defer portsInUseLock.Unlock()
|
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
// generate port between 10000 and 65535
|
|
||||||
rN, err := rng.Number(55535)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("filter: failed to generate random port: %s", err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
port := uint16(rN + 10000)
|
|
||||||
|
|
||||||
// check if free, return if it is
|
|
||||||
_, ok := portsInUse[port]
|
|
||||||
if !ok {
|
|
||||||
portsInUse[port] = &portStatus{
|
|
||||||
lastSeen: time.Now(),
|
|
||||||
isMe: true,
|
|
||||||
}
|
|
||||||
return port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func portsInUseCleaner(ctx context.Context) error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
case <-time.After(cleanerTickDuration):
|
|
||||||
cleanPortsInUse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanPortsInUse() {
|
|
||||||
portsInUseLock.Lock()
|
|
||||||
defer portsInUseLock.Unlock()
|
|
||||||
|
|
||||||
threshold := time.Now().Add(-cleanTimeout)
|
|
||||||
|
|
||||||
for port, status := range portsInUse {
|
|
||||||
if status.lastSeen.Before(threshold) {
|
|
||||||
delete(portsInUse, port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
113
firewall/preauth.go
Normal file
113
firewall/preauth.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package firewall
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/network"
|
||||||
|
"github.com/safing/portmaster/network/packet"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
|
"github.com/safing/portmaster/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
preAuthenticatedPorts = make(map[string]struct{})
|
||||||
|
preAuthenticatedPortsLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
resolver.SetLocalAddrFactory(PermittedAddr)
|
||||||
|
netenv.SetLocalAddrFactory(PermittedAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermittedAddr returns an already permitted local address for the given network for reliable connectivity.
|
||||||
|
// Returns nil in case of error.
|
||||||
|
func PermittedAddr(network string) net.Addr {
|
||||||
|
switch network {
|
||||||
|
case "udp":
|
||||||
|
return PermittedUDPAddr()
|
||||||
|
case "tcp":
|
||||||
|
return PermittedTCPAddr()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermittedUDPAddr returns an already permitted local udp address for reliable connectivity.
|
||||||
|
// Returns nil in case of error.
|
||||||
|
func PermittedUDPAddr() *net.UDPAddr {
|
||||||
|
preAuthdPort := GetPermittedPort(packet.UDP)
|
||||||
|
if preAuthdPort == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", preAuthdPort))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermittedTCPAddr returns an already permitted local tcp address for reliable connectivity.
|
||||||
|
// Returns nil in case of error.
|
||||||
|
func PermittedTCPAddr() *net.TCPAddr {
|
||||||
|
preAuthdPort := GetPermittedPort(packet.TCP)
|
||||||
|
if preAuthdPort == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", preAuthdPort))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPermittedPort returns a local port number that is already permitted for communication.
|
||||||
|
// This bypasses the process attribution step to guarantee connectivity.
|
||||||
|
// Communication on the returned port is attributed to the Portmaster.
|
||||||
|
// Every pre-authenticated port is only valid once.
|
||||||
|
// If no unused local port number can be found, it will return 0, which is
|
||||||
|
// expected to trigger automatic port selection by the underlying OS.
|
||||||
|
func GetPermittedPort(protocol packet.IPProtocol) uint16 {
|
||||||
|
port, ok := network.GetUnusedLocalPort(uint8(protocol))
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
preAuthenticatedPortsLock.Lock()
|
||||||
|
defer preAuthenticatedPortsLock.Unlock()
|
||||||
|
|
||||||
|
// Save generated port.
|
||||||
|
key := generateLocalPreAuthKey(uint8(protocol), port)
|
||||||
|
preAuthenticatedPorts[key] = struct{}{}
|
||||||
|
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
// localPortIsPreAuthenticated checks if the given protocol and port are
|
||||||
|
// pre-authenticated and should be attributed to the Portmaster itself.
|
||||||
|
func localPortIsPreAuthenticated(protocol uint8, port uint16) bool {
|
||||||
|
preAuthenticatedPortsLock.Lock()
|
||||||
|
defer preAuthenticatedPortsLock.Unlock()
|
||||||
|
|
||||||
|
// Check if the given protocol and port are pre-authenticated.
|
||||||
|
key := generateLocalPreAuthKey(protocol, port)
|
||||||
|
_, ok := preAuthenticatedPorts[key]
|
||||||
|
if ok {
|
||||||
|
// Immediately remove pre authenticated port.
|
||||||
|
delete(preAuthenticatedPorts, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateLocalPreAuthKey creates a map key for the pre-authenticated ports.
|
||||||
|
func generateLocalPreAuthKey(protocol uint8, port uint16) string {
|
||||||
|
return strconv.Itoa(int(protocol)) + ":" + strconv.Itoa(int(port))
|
||||||
|
}
|
49
network/ports.go
Normal file
49
network/ports.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/rng"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUnusedLocalPort returns a local port of the specified protocol that is
|
||||||
|
// currently unused and is unlikely to be used within the next seconds.
|
||||||
|
func GetUnusedLocalPort(protocol uint8) (port uint16, ok bool) {
|
||||||
|
allConns := conns.clone()
|
||||||
|
|
||||||
|
tries := 1000
|
||||||
|
hundredth := tries / 100
|
||||||
|
|
||||||
|
// Try up to 1000 times to find an unused port.
|
||||||
|
nextPort:
|
||||||
|
for i := 0; i < tries; i++ {
|
||||||
|
// Generate random port between 10000 and 65535
|
||||||
|
rN, err := rng.Number(55535)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("network: failed to generate random port: %s", err)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
port := uint16(rN + 10000)
|
||||||
|
|
||||||
|
// Shrink range when we chew through the tries.
|
||||||
|
portRangeStart := port - uint16(100-(i/hundredth))
|
||||||
|
|
||||||
|
// Check if the generated port is unused.
|
||||||
|
nextConnection:
|
||||||
|
for _, conn := range allConns {
|
||||||
|
// Skip connection if the protocol does not match the protocol of interest.
|
||||||
|
if conn.Entity.Protocol != protocol {
|
||||||
|
continue nextConnection
|
||||||
|
}
|
||||||
|
// Skip port if the local port is in dangerous proximity.
|
||||||
|
// Consecutive port numbers are very common.
|
||||||
|
if conn.LocalPort <= port && conn.LocalPort >= portRangeStart {
|
||||||
|
continue nextPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The checks have passed. We have found a good unused port.
|
||||||
|
return port, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
72
ui/serve.go
72
ui/serve.go
|
@ -3,7 +3,6 @@ package ui
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -143,7 +142,7 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri
|
||||||
// set content type
|
// set content type
|
||||||
_, ok := w.Header()["Content-Type"]
|
_, ok := w.Header()["Content-Type"]
|
||||||
if !ok {
|
if !ok {
|
||||||
contentType := mime.TypeByExtension(filepath.Ext(path))
|
contentType := mimeTypeByExtension(filepath.Ext(path))
|
||||||
if contentType != "" {
|
if contentType != "" {
|
||||||
w.Header().Set("Content-Type", contentType)
|
w.Header().Set("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
|
@ -179,3 +178,72 @@ func redirectToDefault(w http.ResponseWriter, r *http.Request) {
|
||||||
func redirAddSlash(w http.ResponseWriter, r *http.Request) {
|
func redirAddSlash(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusPermanentRedirect)
|
http.Redirect(w, r, r.RequestURI+"/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We now do our mimetypes ourselves, because, as far as we analyzed, a Windows
|
||||||
|
// update screwed us over here and broke all the mime typing.
|
||||||
|
// (April 2021)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultMimeType = "application/octet-stream"
|
||||||
|
|
||||||
|
mimeTypes = map[string]string{
|
||||||
|
".7z": "application/x-7z-compressed",
|
||||||
|
".atom": "application/atom+xml",
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".csv": "text/csv; charset=utf-8",
|
||||||
|
".deb": "application/x-debian-package",
|
||||||
|
".epub": "application/epub+zip",
|
||||||
|
".es": "application/ecmascript",
|
||||||
|
".flv": "video/x-flv",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".gz": "application/gzip",
|
||||||
|
".htm": "text/html; charset=utf-8",
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".js": "text/javascript; charset=utf-8",
|
||||||
|
".json": "application/json",
|
||||||
|
".m3u": "audio/mpegurl",
|
||||||
|
".m4a": "audio/mpeg",
|
||||||
|
".md": "text/markdown; charset=utf-8",
|
||||||
|
".mjs": "text/javascript; charset=utf-8",
|
||||||
|
".mov": "video/quicktime",
|
||||||
|
".mp3": "audio/mpeg",
|
||||||
|
".mp4": "video/mp4",
|
||||||
|
".mpeg": "video/mpeg",
|
||||||
|
".mpg": "video/mpeg",
|
||||||
|
".ogg": "audio/ogg",
|
||||||
|
".ogv": "video/ogg",
|
||||||
|
".otf": "font/otf",
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
".png": "image/png",
|
||||||
|
".qt": "video/quicktime",
|
||||||
|
".rar": "application/rar",
|
||||||
|
".rtf": "application/rtf",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".tar": "application/x-tar",
|
||||||
|
".tiff": "image/tiff",
|
||||||
|
".ts": "video/MP2T",
|
||||||
|
".ttc": "font/collection",
|
||||||
|
".ttf": "font/ttf",
|
||||||
|
".txt": "text/plain; charset=utf-8",
|
||||||
|
".wasm": "application/wasm",
|
||||||
|
".wav": "audio/x-wav",
|
||||||
|
".webm": "video/webm",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".woff": "font/woff",
|
||||||
|
".woff2": "font/woff2",
|
||||||
|
".xml": "text/xml; charset=utf-8",
|
||||||
|
".xz": "application/x-xz",
|
||||||
|
".zip": "application/zip",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func mimeTypeByExtension(ext string) string {
|
||||||
|
mimeType, ok := mimeTypes[ext]
|
||||||
|
if ok {
|
||||||
|
return mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultMimeType
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue